Move builds to prod-pins and add explicit update channels
Some checks failed
Validate authentication / Validate authentication (push) Failing after 1s
Some checks failed
Validate authentication / Validate authentication (push) Failing after 1s
This commit is contained in:
parent
3d824fde58
commit
e71d9c3118
15 changed files with 473 additions and 722 deletions
|
|
@ -1,248 +0,0 @@
|
||||||
name: Build & Publish Release
|
|
||||||
|
|
||||||
env:
|
|
||||||
NIX_CONFIG: experimental-features = nix-command flakes
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
release_type:
|
|
||||||
description: "Release type: 'dev' or 'stable'"
|
|
||||||
required: true
|
|
||||||
default: "dev"
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- dev
|
|
||||||
- stable
|
|
||||||
description:
|
|
||||||
description: "Release description"
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
release_sequence:
|
|
||||||
description: "Monotonic sequence allocated for this update channel"
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
expires_at:
|
|
||||||
description: "Signed manifest expiry in RFC 3339 format"
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: iota-release-${{ inputs.release_type }}
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build & Publish Release
|
|
||||||
runs-on: host
|
|
||||||
steps:
|
|
||||||
- name: Set up repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
env:
|
|
||||||
DOCKER_USER: ${{ secrets.DOCKER_USER }}
|
|
||||||
DOCKER_PASSWD: ${{ secrets.DOCKER_PASSWD }}
|
|
||||||
run: |
|
|
||||||
set -eu
|
|
||||||
DOCKER_USER="$(printf '%s' "$DOCKER_USER" | tr -d '\r\n')"
|
|
||||||
DOCKER_PASSWD="$(printf '%s' "$DOCKER_PASSWD" | tr -d '\r\n')"
|
|
||||||
printf '%s' "$DOCKER_PASSWD" | nix-shell -p docker --run "docker login docker.io --username \"$DOCKER_USER\" --password-stdin"
|
|
||||||
|
|
||||||
- name: Build & Push Docker image
|
|
||||||
run: |
|
|
||||||
nix-shell -p docker --run "docker build -f dockerfile -t tensamin/iota:latest . && docker push tensamin/iota:latest"
|
|
||||||
|
|
||||||
- name: Read release metadata
|
|
||||||
id: version
|
|
||||||
env:
|
|
||||||
RELEASE_TYPE: ${{ inputs.release_type }}
|
|
||||||
run: |
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
VERSION="$(nix eval --raw .#default.version)"
|
|
||||||
SHORT_SHA="$(git rev-parse --short=7 HEAD)"
|
|
||||||
|
|
||||||
case "$RELEASE_TYPE" in
|
|
||||||
dev)
|
|
||||||
TAG="${VERSION}-dev-${SHORT_SHA}"
|
|
||||||
PRERELEASE="true"
|
|
||||||
;;
|
|
||||||
stable)
|
|
||||||
TAG="$VERSION"
|
|
||||||
PRERELEASE="false"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "release_type must be either 'dev' or 'stable'"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "version=$VERSION" >> "$FORGEJO_OUTPUT"
|
|
||||||
echo "tag=$TAG" >> "$FORGEJO_OUTPUT"
|
|
||||||
echo "title=$TAG" >> "$FORGEJO_OUTPUT"
|
|
||||||
echo "prerelease=$PRERELEASE" >> "$FORGEJO_OUTPUT"
|
|
||||||
ARCH="$(uname -m)"
|
|
||||||
echo "arch=$ARCH" >> "$FORGEJO_OUTPUT"
|
|
||||||
echo "asset_name=iota-${TAG}-linux-${ARCH}.zip" >> "$FORGEJO_OUTPUT"
|
|
||||||
echo "update_manifest_name=iota-update-${TAG}-linux-${ARCH}.json" >> "$FORGEJO_OUTPUT"
|
|
||||||
echo "channel_tag=iota-updates-${RELEASE_TYPE}-linux-${ARCH}" >> "$FORGEJO_OUTPUT"
|
|
||||||
echo "channel_manifest_name=iota-update-linux-${ARCH}.json" >> "$FORGEJO_OUTPUT"
|
|
||||||
|
|
||||||
- name: Build and validate installer bundle
|
|
||||||
env:
|
|
||||||
TAG: ${{ steps.version.outputs.tag }}
|
|
||||||
ASSET_NAME: ${{ steps.version.outputs.asset_name }}
|
|
||||||
ARCH: ${{ steps.version.outputs.arch }}
|
|
||||||
UPDATE_MANIFEST_NAME: ${{ steps.version.outputs.update_manifest_name }}
|
|
||||||
CHANNEL_TAG: ${{ steps.version.outputs.channel_tag }}
|
|
||||||
CHANNEL_MANIFEST_NAME: ${{ steps.version.outputs.channel_manifest_name }}
|
|
||||||
RELEASE_TYPE: ${{ inputs.release_type }}
|
|
||||||
RELEASE_SEQUENCE: ${{ inputs.release_sequence }}
|
|
||||||
EXPIRES_AT: ${{ inputs.expires_at }}
|
|
||||||
SERVER_URL: ${{ forgejo.server_url }}
|
|
||||||
REPO: ${{ forgejo.repository }}
|
|
||||||
TOKEN: ${{ forgejo.token }}
|
|
||||||
IOTA_RELEASE_SIGNING_KEY: ${{ secrets.IOTA_RELEASE_SIGNING_KEY }}
|
|
||||||
run: |
|
|
||||||
set -eu
|
|
||||||
: "${IOTA_RELEASE_SIGNING_KEY:?IOTA_RELEASE_SIGNING_KEY secret is required}"
|
|
||||||
|
|
||||||
nix build "git+file://$PWD?submodules=1#default" --print-build-logs
|
|
||||||
mkdir -p dist/bin
|
|
||||||
install -m755 result/bin/iota dist/bin/iota
|
|
||||||
install -m755 result/bin/iota-daemon dist/bin/iota-daemon
|
|
||||||
install -m755 result/bin/iota-updater dist/bin/iota-updater
|
|
||||||
|
|
||||||
UPDATE_BASE_URL="${SERVER_URL%/}/${REPO}/releases/download/${TAG}"
|
|
||||||
UPDATE_CHANNEL_BASE_URL="${SERVER_URL%/}/${REPO}/releases/download/${CHANNEL_TAG}"
|
|
||||||
export UPDATE_CHANNEL_BASE_URL
|
|
||||||
nix-shell -p curl jq --run '
|
|
||||||
set -eu
|
|
||||||
CHANNEL_STATUS="$(curl -L -sS -w "%{http_code}" -o previous-channel-manifest.json \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
"$UPDATE_CHANNEL_BASE_URL/$CHANNEL_MANIFEST_NAME")"
|
|
||||||
case "$CHANNEL_STATUS" in
|
|
||||||
200)
|
|
||||||
jq -e \
|
|
||||||
--argjson proposed "$RELEASE_SEQUENCE" \
|
|
||||||
--arg version "$TAG" \
|
|
||||||
"(.release_sequence < \$proposed) or (.release_sequence == \$proposed and .product_version == \$version)" \
|
|
||||||
previous-channel-manifest.json >/dev/null || {
|
|
||||||
echo "release sequence must increase, or identify the same release during a refresh"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
;;
|
|
||||||
404) ;;
|
|
||||||
*)
|
|
||||||
echo "could not read current channel manifest: HTTP $CHANNEL_STATUS"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
'
|
|
||||||
PUBLISHED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
||||||
export ARCH EXPIRES_AT PUBLISHED_AT RELEASE_SEQUENCE RELEASE_TYPE TAG UPDATE_BASE_URL UPDATE_MANIFEST_NAME
|
|
||||||
nix-shell -p coreutils jq --run 'bash scripts/build-update-manifest.sh dist/bin "$TAG" "$RELEASE_TYPE" "$RELEASE_SEQUENCE" "$PUBLISHED_AT" "$EXPIRES_AT" linux "$ARCH" "$UPDATE_BASE_URL" "dist/$UPDATE_MANIFEST_NAME"'
|
|
||||||
UPDATE_PUBLIC_KEY="$(result/bin/iota-release sign "dist/$UPDATE_MANIFEST_NAME" "dist/$UPDATE_MANIFEST_NAME.sig")"
|
|
||||||
export UPDATE_PUBLIC_KEY
|
|
||||||
nix-shell -p jq zip --run 'bash scripts/build-release-bundle.sh \
|
|
||||||
dist/bin \
|
|
||||||
"$TAG" \
|
|
||||||
"dist/$UPDATE_MANIFEST_NAME" \
|
|
||||||
"$UPDATE_CHANNEL_BASE_URL/$CHANNEL_MANIFEST_NAME" \
|
|
||||||
"$UPDATE_PUBLIC_KEY" \
|
|
||||||
"$UPDATE_CHANNEL_BASE_URL/$CHANNEL_MANIFEST_NAME.sig" \
|
|
||||||
"$RELEASE_TYPE" \
|
|
||||||
primary \
|
|
||||||
"dist/$ASSET_NAME"'
|
|
||||||
result/bin/iota-bundle "dist/$ASSET_NAME"
|
|
||||||
|
|
||||||
- name: Create release and upload release assets
|
|
||||||
env:
|
|
||||||
TOKEN: ${{ forgejo.token }}
|
|
||||||
API: ${{ forgejo.api_url }}
|
|
||||||
REPO: ${{ forgejo.repository }}
|
|
||||||
SHA: ${{ forgejo.sha }}
|
|
||||||
TAG: ${{ steps.version.outputs.tag }}
|
|
||||||
TITLE: ${{ steps.version.outputs.title }}
|
|
||||||
PRERELEASE: ${{ steps.version.outputs.prerelease }}
|
|
||||||
ASSET_NAME: ${{ steps.version.outputs.asset_name }}
|
|
||||||
UPDATE_MANIFEST_NAME: ${{ steps.version.outputs.update_manifest_name }}
|
|
||||||
CHANNEL_TAG: ${{ steps.version.outputs.channel_tag }}
|
|
||||||
CHANNEL_MANIFEST_NAME: ${{ steps.version.outputs.channel_manifest_name }}
|
|
||||||
RELEASE_TYPE: ${{ inputs.release_type }}
|
|
||||||
DESCRIPTION: ${{ inputs.description }}
|
|
||||||
run: |
|
|
||||||
nix-shell -p curl jq --run '
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
HTTP_STATUS=$(curl -s -w "%{http_code}" -o release_out.json \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
"$API/repos/$REPO/releases/tags/$TAG")
|
|
||||||
|
|
||||||
if [ "$HTTP_STATUS" = "200" ]; then
|
|
||||||
echo "Release $TAG already exists."
|
|
||||||
RELEASE_ID="$(jq -r .id release_out.json)"
|
|
||||||
else
|
|
||||||
echo "Creating release for $TAG"
|
|
||||||
RELEASE_JSON="$(curl -f -sS -X POST "$API/repos/$REPO/releases" \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$(jq -n \
|
|
||||||
--arg tag "$TAG" \
|
|
||||||
--arg name "$TITLE" \
|
|
||||||
--arg body "$DESCRIPTION" \
|
|
||||||
--arg target "$SHA" \
|
|
||||||
--argjson prerelease "$PRERELEASE" \
|
|
||||||
'"'"'{ tag_name: $tag, name: $name, body: $body, target_commitish: $target, draft: false, prerelease: $prerelease }'"'"')")"
|
|
||||||
RELEASE_ID="$(echo "$RELEASE_JSON" | jq -r .id)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
upload_asset() {
|
|
||||||
TARGET_RELEASE_ID="$1"
|
|
||||||
ASSET="$2"
|
|
||||||
PATHNAME="$3"
|
|
||||||
curl -fsS -X POST "$API/repos/$REPO/releases/$TARGET_RELEASE_ID/assets?name=$ASSET" \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
-F "attachment=@$PATHNAME"
|
|
||||||
}
|
|
||||||
|
|
||||||
upload_asset "$RELEASE_ID" iota dist/bin/iota
|
|
||||||
upload_asset "$RELEASE_ID" iota-daemon dist/bin/iota-daemon
|
|
||||||
upload_asset "$RELEASE_ID" iota-updater dist/bin/iota-updater
|
|
||||||
upload_asset "$RELEASE_ID" "$UPDATE_MANIFEST_NAME.sig" "dist/$UPDATE_MANIFEST_NAME.sig"
|
|
||||||
upload_asset "$RELEASE_ID" "$ASSET_NAME" "dist/$ASSET_NAME"
|
|
||||||
upload_asset "$RELEASE_ID" "$UPDATE_MANIFEST_NAME" "dist/$UPDATE_MANIFEST_NAME"
|
|
||||||
|
|
||||||
CHANNEL_STATUS=$(curl -s -w "%{http_code}" -o channel_out.json \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
"$API/repos/$REPO/releases/tags/$CHANNEL_TAG")
|
|
||||||
if [ "$CHANNEL_STATUS" = "200" ]; then
|
|
||||||
CHANNEL_RELEASE_ID="$(jq -r .id channel_out.json)"
|
|
||||||
else
|
|
||||||
CHANNEL_JSON="$(curl -f -sS -X POST "$API/repos/$REPO/releases" \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$(jq -n \
|
|
||||||
--arg tag "$CHANNEL_TAG" \
|
|
||||||
--arg name "Iota $RELEASE_TYPE update channel" \
|
|
||||||
--arg target "$SHA" \
|
|
||||||
'"'"'{ tag_name: $tag, name: $name, body: "Managed by the release workflow.", target_commitish: $target, draft: false, prerelease: true }'"'"')")"
|
|
||||||
CHANNEL_RELEASE_ID="$(echo "$CHANNEL_JSON" | jq -r .id)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
CHANNEL_ASSETS="$(curl -fsS \
|
|
||||||
-H "Authorization: token $TOKEN" \
|
|
||||||
"$API/repos/$REPO/releases/$CHANNEL_RELEASE_ID/assets")"
|
|
||||||
for CHANNEL_ASSET in "$CHANNEL_MANIFEST_NAME" "$CHANNEL_MANIFEST_NAME.sig"; do
|
|
||||||
echo "$CHANNEL_ASSETS" | jq -r --arg name "$CHANNEL_ASSET" '"'"'.[] | select(.name == $name) | .id'"'"' | while read -r ASSET_ID; do
|
|
||||||
[ -n "$ASSET_ID" ] || continue
|
|
||||||
curl -fsS -X DELETE "$API/repos/$REPO/releases/assets/$ASSET_ID" \
|
|
||||||
-H "Authorization: token $TOKEN"
|
|
||||||
done
|
|
||||||
done
|
|
||||||
upload_asset "$CHANNEL_RELEASE_ID" "$CHANNEL_MANIFEST_NAME.sig" "dist/$UPDATE_MANIFEST_NAME.sig"
|
|
||||||
upload_asset "$CHANNEL_RELEASE_ID" "$CHANNEL_MANIFEST_NAME" "dist/$UPDATE_MANIFEST_NAME"
|
|
||||||
'
|
|
||||||
|
|
@ -32,3 +32,12 @@ jobs:
|
||||||
|
|
||||||
- name: Check release binaries
|
- name: Check release binaries
|
||||||
run: nix develop -c cargo check -p iota -p iota-daemon -p iota-updater -p iota-installer --locked
|
run: nix develop -c cargo check -p iota -p iota-daemon -p iota-updater -p iota-installer --locked
|
||||||
|
|
||||||
|
- name: Check updater and installer formatting
|
||||||
|
run: nix develop -c cargo fmt -p iota-updater -p iota-installer -p iota --check
|
||||||
|
|
||||||
|
- name: Run updater and installer tests
|
||||||
|
run: nix develop -c cargo test -p iota-updater -p iota-installer --locked
|
||||||
|
|
||||||
|
- name: Check release scripts
|
||||||
|
run: bash -n scripts/build-update-manifest.sh scripts/build-release-bundle.sh
|
||||||
|
|
|
||||||
155
flake.lock
generated
155
flake.lock
generated
|
|
@ -1,21 +1,54 @@
|
||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"flake-parts": {
|
"client": {
|
||||||
"inputs": {
|
"flake": false,
|
||||||
"nixpkgs-lib": "nixpkgs-lib"
|
|
||||||
},
|
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1782949081,
|
"lastModified": 1791114583,
|
||||||
"narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
|
"narHash": "sha256-J4j6LI+erWFp+maryBRN08Ra90BKLjv0NOhVC7subEY=",
|
||||||
"owner": "hercules-ci",
|
"ref": "dev",
|
||||||
"repo": "flake-parts",
|
"rev": "d08a00a94acda0d1622de5960fa74750bf7d64db",
|
||||||
"rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
|
"revCount": 646,
|
||||||
"type": "github"
|
"type": "git",
|
||||||
|
"url": "ssh://git@methanium.net/tensamin/client"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "hercules-ci",
|
"ref": "dev",
|
||||||
"repo": "flake-parts",
|
"type": "git",
|
||||||
"type": "github"
|
"url": "ssh://git@methanium.net/tensamin/client"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"iota": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1791114268,
|
||||||
|
"narHash": "sha256-iKNN+La/hAKXxJL6wYti2jlZ4uS2C5dRkQyuVnciPwU=",
|
||||||
|
"ref": "main",
|
||||||
|
"rev": "3d824fde58f1a9ff3c2df45311f7dc658e9700dd",
|
||||||
|
"revCount": 327,
|
||||||
|
"type": "git",
|
||||||
|
"url": "ssh://git@methanium.net/tensamin/iota"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"ref": "main",
|
||||||
|
"type": "git",
|
||||||
|
"url": "ssh://git@methanium.net/tensamin/iota"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mtp": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1791113691,
|
||||||
|
"narHash": "sha256-r7LAe6KUuVCXLzyTNo1vqQeXxmfsXuzFV+qV6teZd+Y=",
|
||||||
|
"ref": "master",
|
||||||
|
"rev": "ad489265deacadd6de52ed2129d071803a0e7247",
|
||||||
|
"revCount": 215,
|
||||||
|
"type": "git",
|
||||||
|
"url": "ssh://git@methanium.net/methanium/mtp"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"ref": "master",
|
||||||
|
"type": "git",
|
||||||
|
"url": "ssh://git@methanium.net/methanium/mtp"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mtp-type-maps": {
|
"mtp-type-maps": {
|
||||||
|
|
@ -23,68 +56,112 @@
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1790572556,
|
"lastModified": 1790572556,
|
||||||
"narHash": "sha256-ugNZR2Be9N9UjG0aVN8Yrk4hYOVC2edjtC9xBhbW35w=",
|
"narHash": "sha256-ugNZR2Be9N9UjG0aVN8Yrk4hYOVC2edjtC9xBhbW35w=",
|
||||||
"ref": "refs/heads/main",
|
"ref": "main",
|
||||||
"rev": "4f18c7a0d9b04d38a77fbb011c4f0b21c25bf7bf",
|
"rev": "4f18c7a0d9b04d38a77fbb011c4f0b21c25bf7bf",
|
||||||
"revCount": 28,
|
"revCount": 28,
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.methanium.net/tensamin/mtp-type-maps"
|
"url": "ssh://git@methanium.net/tensamin/mtp-type-maps"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
"ref": "main",
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.methanium.net/tensamin/mtp-type-maps"
|
"url": "ssh://git@methanium.net/tensamin/mtp-type-maps"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1784497964,
|
"lastModified": 1790981744,
|
||||||
"narHash": "sha256-vlHUuqAcbcH2RKmHbPiuQzbv1pnzzavXnI62RD0bqCU=",
|
"narHash": "sha256-sm6DclXJudZfP/pcDBQQsRqyMxBcQsuw+7yQr4rBBLE=",
|
||||||
"owner": "nixos",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "241313f4e8e508cb9b13278c2b0fa25b9ca27163",
|
"rev": "55ba7f49ef2962b42cbd126522b7df5f95037679",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nixos",
|
"owner": "NixOS",
|
||||||
"ref": "nixos-unstable",
|
"ref": "nixpkgs-unstable",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs-lib": {
|
"omega": {
|
||||||
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1782614948,
|
"lastModified": 1791114269,
|
||||||
"narHash": "sha256-ePjCwr1sNm9NYUqywL7QfK3JnlS015msC+eBu2zKlp8=",
|
"narHash": "sha256-56Nh5XnH7SK1P0kdtai/ZKcuQr8YlgDo5ndBf67YnFo=",
|
||||||
"owner": "nix-community",
|
"ref": "main",
|
||||||
"repo": "nixpkgs.lib",
|
"rev": "993fa5ead0cfe5f5f0ab1ecd12ffe0f343380489",
|
||||||
"rev": "db3f255737b94216eb71cce308e2912cf6bc2d7c",
|
"revCount": 157,
|
||||||
"type": "github"
|
"type": "git",
|
||||||
|
"url": "ssh://git@methanium.net/tensamin/omega"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nix-community",
|
"ref": "main",
|
||||||
"repo": "nixpkgs.lib",
|
"type": "git",
|
||||||
"type": "github"
|
"url": "ssh://git@methanium.net/tensamin/omega"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"omikron": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1791114268,
|
||||||
|
"narHash": "sha256-x6jc6l3LC3jTG/5YOuch32spGXfUzhO20M3g/Qmq2FY=",
|
||||||
|
"ref": "main",
|
||||||
|
"rev": "39371ad398d13aa1792c1ff58d2fb72f7be15787",
|
||||||
|
"revCount": 211,
|
||||||
|
"type": "git",
|
||||||
|
"url": "ssh://git@methanium.net/tensamin/omikron"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"ref": "main",
|
||||||
|
"type": "git",
|
||||||
|
"url": "ssh://git@methanium.net/tensamin/omikron"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prod-pins": {
|
||||||
|
"inputs": {
|
||||||
|
"client": "client",
|
||||||
|
"iota": "iota",
|
||||||
|
"mtp": "mtp",
|
||||||
|
"mtp-type-maps": "mtp-type-maps",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"omega": "omega",
|
||||||
|
"omikron": "omikron",
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1791134396,
|
||||||
|
"narHash": "sha256-2HDKbqt9xNt7shkkASMmP7UGyz69NO/23j4IOXxADEs=",
|
||||||
|
"ref": "main",
|
||||||
|
"rev": "123205c97d1c75977ada8a3a80bd8323e19773bb",
|
||||||
|
"revCount": 3,
|
||||||
|
"type": "git",
|
||||||
|
"url": "ssh://git@methanium.net/tensamin/prod-pins"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"ref": "main",
|
||||||
|
"type": "git",
|
||||||
|
"url": "ssh://git@methanium.net/tensamin/prod-pins"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-parts": "flake-parts",
|
"prod-pins": "prod-pins"
|
||||||
"mtp-type-maps": "mtp-type-maps",
|
|
||||||
"nixpkgs": "nixpkgs",
|
|
||||||
"rust-overlay": "rust-overlay"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rust-overlay": {
|
"rust-overlay": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
|
"prod-pins",
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1784526465,
|
"lastModified": 1791101754,
|
||||||
"narHash": "sha256-L37teKC6oINWG4PGZLIqbphMWvSQ0PEz+aWxAk+rIDw=",
|
"narHash": "sha256-y/GF+9B0t0TZ0nQiSRMqDXpr76yT7sdDbS/3GNLhzkE=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "58c6334db52d51fc5dd8877c90b01f00cf8a696b",
|
"rev": "dbc715a4b7c0ace63b9769a032d1dd34cd89e5bd",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
399
flake.nix
399
flake.nix
|
|
@ -1,389 +1,34 @@
|
||||||
{
|
{
|
||||||
description = "Iota";
|
description = "Iota development and verification";
|
||||||
|
|
||||||
inputs = {
|
inputs.prod-pins.url = "git+ssh://git@methanium.net/tensamin/prod-pins?ref=main";
|
||||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
|
||||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
|
||||||
rust-overlay = {
|
|
||||||
url = "github:oxalica/rust-overlay";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
mtp-type-maps = {
|
|
||||||
url = "git+https://git.methanium.net/tensamin/mtp-type-maps";
|
|
||||||
flake = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs =
|
outputs = { self, prod-pins, ... }:
|
||||||
inputs@{
|
|
||||||
self,
|
|
||||||
nixpkgs,
|
|
||||||
flake-parts,
|
|
||||||
rust-overlay,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
|
||||||
systems = [
|
|
||||||
"x86_64-linux"
|
|
||||||
"aarch64-linux"
|
|
||||||
"x86_64-darwin"
|
|
||||||
"aarch64-darwin"
|
|
||||||
];
|
|
||||||
|
|
||||||
perSystem =
|
|
||||||
{
|
|
||||||
self',
|
|
||||||
pkgs,
|
|
||||||
system,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
let
|
||||||
rustPkgs = import nixpkgs {
|
forSystems = prod-pins.inputs.nixpkgs.lib.genAttrs [ "x86_64-linux" "aarch64-linux" ];
|
||||||
|
project = system: prod-pins.lib.mkPackages {
|
||||||
inherit system;
|
inherit system;
|
||||||
overlays = [ (import rust-overlay) ];
|
sources.iota = self;
|
||||||
};
|
};
|
||||||
rustToolchain = rustPkgs.rust-bin.stable.latest.default.override {
|
in {
|
||||||
extensions = [
|
checks = forSystems (system: { inherit ((project system).packages) iota; });
|
||||||
"rust-src"
|
devShells = forSystems (system: {
|
||||||
"rust-analyzer"
|
default = (project system).devShells.iota.overrideAttrs (old: {
|
||||||
"clippy"
|
TENSAMIN_SOURCE = (project system).packages.iota.transformedSource;
|
||||||
"rustfmt"
|
shellHook = (old.shellHook or "") + ''
|
||||||
];
|
# Stage authoritative dependencies without changing the checkout.
|
||||||
};
|
export TENSAMIN_CHECKOUT="$PWD"
|
||||||
commonBuildInputs = with pkgs; [
|
workRoot="/tmp/tensamin-dev-$UID/$(basename "$TENSAMIN_SOURCE")"
|
||||||
openssl
|
if [ ! -d "$workRoot" ]; then
|
||||||
sqlite
|
mkdir -p "$(dirname "$workRoot")"
|
||||||
];
|
stage="$(mktemp -d "$workRoot.XXXXXX")"
|
||||||
commonNativeBuildInputs = with pkgs; [
|
cp -R "$TENSAMIN_SOURCE/." "$stage/" &&
|
||||||
cmake
|
chmod -R u+w "$stage" &&
|
||||||
perl
|
mv -T "$stage" "$workRoot" || exit 1
|
||||||
pkg-config
|
|
||||||
];
|
|
||||||
in
|
|
||||||
{
|
|
||||||
packages = {
|
|
||||||
default = pkgs.rustPlatform.buildRustPackage {
|
|
||||||
pname = "iota";
|
|
||||||
version = "0.1.0";
|
|
||||||
src = ./.;
|
|
||||||
cargoBuildFlags = [
|
|
||||||
"-p"
|
|
||||||
"iota"
|
|
||||||
"-p"
|
|
||||||
"iota-daemon"
|
|
||||||
"-p"
|
|
||||||
"iota-updater"
|
|
||||||
"-p"
|
|
||||||
"iota-installer"
|
|
||||||
];
|
|
||||||
cargoLock = {
|
|
||||||
lockFile = ./Cargo.lock;
|
|
||||||
allowBuiltinFetchGit = true;
|
|
||||||
};
|
|
||||||
nativeBuildInputs = commonNativeBuildInputs;
|
|
||||||
buildInputs = commonBuildInputs;
|
|
||||||
dontUseCmakeConfigure = true;
|
|
||||||
MTP_TYPE_MAPS = "${inputs.mtp-type-maps}/type-maps.yaml";
|
|
||||||
passthru.dataDir = "/var/lib/iota";
|
|
||||||
};
|
|
||||||
|
|
||||||
iota-daemon = self'.packages.default.overrideAttrs (old: {
|
|
||||||
pname = "iota-daemon";
|
|
||||||
cargoBuildFlags = [
|
|
||||||
"-p"
|
|
||||||
"iota-daemon"
|
|
||||||
];
|
|
||||||
postInstall = ''
|
|
||||||
for f in $out/bin/*; do
|
|
||||||
if [ "$(basename "$f")" != "iota-daemon" ]; then
|
|
||||||
rm "$f"
|
|
||||||
fi
|
fi
|
||||||
done
|
cd "$workRoot"
|
||||||
'';
|
'';
|
||||||
});
|
});
|
||||||
|
|
||||||
iota-ui = self'.packages.default.overrideAttrs (old: {
|
|
||||||
pname = "iota-ui";
|
|
||||||
cargoBuildFlags = [
|
|
||||||
"-p"
|
|
||||||
"iota"
|
|
||||||
];
|
|
||||||
postInstall = ''
|
|
||||||
for f in $out/bin/*; do
|
|
||||||
if [ "$(basename "$f")" != "iota" ]; then
|
|
||||||
rm "$f"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ -f "$out/bin/iota" ]; then
|
|
||||||
mv "$out/bin/iota" "$out/bin/iota-ui"
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
devShells.default = pkgs.mkShell {
|
|
||||||
nativeBuildInputs = with pkgs; [
|
|
||||||
rustToolchain
|
|
||||||
git
|
|
||||||
cmake
|
|
||||||
perl
|
|
||||||
pkg-config
|
|
||||||
];
|
|
||||||
buildInputs = commonBuildInputs;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
flake = {
|
|
||||||
nixosModules.default =
|
|
||||||
{
|
|
||||||
config,
|
|
||||||
pkgs,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
cfg = config.services.iota;
|
|
||||||
defaultPackage =
|
|
||||||
self.packages.${pkgs.stdenv.hostPlatform.system}.default
|
|
||||||
or (throw "iota: no pre-built package for system ${pkgs.stdenv.hostPlatform.system}");
|
|
||||||
|
|
||||||
configFormat = pkgs.formats.yaml { };
|
|
||||||
effectiveSettings = lib.recursiveUpdate {
|
|
||||||
port = cfg.port;
|
|
||||||
web = {
|
|
||||||
mode = "network";
|
|
||||||
bind = cfg.bindAddress;
|
|
||||||
port = cfg.port;
|
|
||||||
required = true;
|
|
||||||
}
|
|
||||||
// lib.optionalAttrs (cfg.certFile != null) {
|
|
||||||
certificate = "${cfg.stateDir}/tls/cert.pem";
|
|
||||||
key = "${cfg.stateDir}/tls/key.pem";
|
|
||||||
};
|
|
||||||
} cfg.settings;
|
|
||||||
sourceConfigFile =
|
|
||||||
if cfg.settingsFile != null then
|
|
||||||
cfg.settingsFile
|
|
||||||
else
|
|
||||||
configFormat.generate "iota-config.yaml" effectiveSettings;
|
|
||||||
configFile = "${cfg.stateDir}/config.yaml";
|
|
||||||
|
|
||||||
descriptionText = "Tensamin Iota daemon";
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.services.iota = {
|
|
||||||
enable = lib.mkEnableOption "Enable the Iota service.";
|
|
||||||
|
|
||||||
stateDir = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "/var/lib/iota";
|
|
||||||
description = "Persistent mutable Iota state.";
|
|
||||||
};
|
|
||||||
cacheDir = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "/var/cache/iota";
|
|
||||||
};
|
|
||||||
runtimeDir = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "/run/iota";
|
|
||||||
};
|
|
||||||
logDir = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "/var/log/iota";
|
|
||||||
};
|
|
||||||
assetDir = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "${cfg.package}/share/iota/web";
|
|
||||||
};
|
|
||||||
|
|
||||||
certFile = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
description = "Path to the SSL certificate file (cert.pem).";
|
|
||||||
};
|
|
||||||
|
|
||||||
keyFile = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
description = "Path to the SSL private key file (cert.key).";
|
|
||||||
};
|
|
||||||
|
|
||||||
environmentFiles = lib.mkOption {
|
|
||||||
type = lib.types.listOf lib.types.str;
|
|
||||||
default = [ ];
|
|
||||||
description = "Environment files to load for the Iota service.";
|
|
||||||
};
|
|
||||||
|
|
||||||
openFirewall = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = true;
|
|
||||||
description = "Whether to open the firewall for ports used by Iota.";
|
|
||||||
};
|
|
||||||
|
|
||||||
bindAddress = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "0.0.0.0";
|
|
||||||
description = "IP address to bind the HTTP server to.";
|
|
||||||
};
|
|
||||||
|
|
||||||
port = lib.mkOption {
|
|
||||||
type = lib.types.port;
|
|
||||||
default = 1984;
|
|
||||||
description = "TCP and UDP port for protocol-only Iota.";
|
|
||||||
};
|
|
||||||
|
|
||||||
omegaApiUrl = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "https://omega.tensamin.net";
|
|
||||||
description = "Omega discovery API URL.";
|
|
||||||
};
|
|
||||||
|
|
||||||
package = lib.mkOption {
|
|
||||||
type = lib.types.package;
|
|
||||||
default = defaultPackage;
|
|
||||||
description = "The Iota package to use.";
|
|
||||||
};
|
|
||||||
|
|
||||||
settings = lib.mkOption {
|
|
||||||
type = lib.types.attrs;
|
|
||||||
default = { };
|
|
||||||
description = "Configuration attributes for Iota, written to YAML.";
|
|
||||||
};
|
|
||||||
|
|
||||||
settingsFile = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.path;
|
|
||||||
default = null;
|
|
||||||
description = "Path to an existing YAML file to use instead of generating from settings.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = (cfg.certFile == null) == (cfg.keyFile == null);
|
|
||||||
message = "services.iota: certFile and keyFile must be set together.";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
users.users.iota = {
|
|
||||||
isSystemUser = true;
|
|
||||||
group = "iota";
|
|
||||||
home = cfg.stateDir;
|
|
||||||
createHome = true;
|
|
||||||
description = "Iota service user";
|
|
||||||
shell = pkgs.bash;
|
|
||||||
};
|
|
||||||
|
|
||||||
users.groups.iota = { };
|
|
||||||
|
|
||||||
systemd.sockets.iota = {
|
|
||||||
description = "${descriptionText} IPC socket";
|
|
||||||
wantedBy = [ "sockets.target" ];
|
|
||||||
socketConfig = {
|
|
||||||
ListenStream = "/run/iota/iota.sock";
|
|
||||||
SocketMode = "0660";
|
|
||||||
SocketUser = "iota";
|
|
||||||
SocketGroup = "iota";
|
|
||||||
DirectoryMode = "0750";
|
|
||||||
Backlog = 5;
|
|
||||||
RemoveOnStop = "true";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.iota = {
|
|
||||||
description = descriptionText;
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
after = [
|
|
||||||
"network.target"
|
|
||||||
"iota.socket"
|
|
||||||
];
|
|
||||||
requires = [ "iota.socket" ];
|
|
||||||
|
|
||||||
environment.OMEGA_API_URL = cfg.omegaApiUrl;
|
|
||||||
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "simple";
|
|
||||||
User = "iota";
|
|
||||||
Group = "iota";
|
|
||||||
ExecStart = "${cfg.package}/bin/iota-daemon";
|
|
||||||
|
|
||||||
Restart = "on-failure";
|
|
||||||
RestartSec = "5s";
|
|
||||||
RuntimeDirectory = "iota";
|
|
||||||
RuntimeDirectoryMode = "0750";
|
|
||||||
RuntimeDirectoryPreserve = "yes";
|
|
||||||
StateDirectory = "iota";
|
|
||||||
StateDirectoryMode = "0750";
|
|
||||||
CacheDirectory = "iota";
|
|
||||||
CacheDirectoryMode = "0750";
|
|
||||||
LogsDirectory = "iota";
|
|
||||||
LogsDirectoryMode = "0750";
|
|
||||||
|
|
||||||
# Exit code 75 = restart requested
|
|
||||||
RestartPreventExitStatus = "0";
|
|
||||||
RestartForceExitStatus = "75";
|
|
||||||
|
|
||||||
TimeoutStopSec = "10";
|
|
||||||
KillMode = "mixed";
|
|
||||||
KillSignal = "SIGTERM";
|
|
||||||
|
|
||||||
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
|
|
||||||
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
|
|
||||||
|
|
||||||
ProtectSystem = "strict";
|
|
||||||
ProtectHome = true;
|
|
||||||
PrivateTmp = true;
|
|
||||||
NoNewPrivileges = true;
|
|
||||||
ReadWritePaths = [
|
|
||||||
cfg.stateDir
|
|
||||||
cfg.cacheDir
|
|
||||||
cfg.runtimeDir
|
|
||||||
cfg.logDir
|
|
||||||
];
|
|
||||||
ReadOnlyPaths = [
|
|
||||||
cfg.assetDir
|
|
||||||
];
|
|
||||||
ProtectKernelTunables = true;
|
|
||||||
ProtectKernelModules = true;
|
|
||||||
ProtectControlGroups = true;
|
|
||||||
RestrictRealtime = true;
|
|
||||||
RestrictSUIDSGID = true;
|
|
||||||
LockPersonality = true;
|
|
||||||
MemoryDenyWriteExecute = true;
|
|
||||||
Environment = [
|
|
||||||
"IOTA_SOCKET=/run/iota/iota.sock"
|
|
||||||
"IOTA_CONFIG_FILE=${configFile}"
|
|
||||||
"IOTA_STATE_DIR=${cfg.stateDir}"
|
|
||||||
"IOTA_CACHE_DIR=${cfg.cacheDir}"
|
|
||||||
"IOTA_RUNTIME_DIR=${cfg.runtimeDir}"
|
|
||||||
"IOTA_LOG_DIR=${cfg.logDir}"
|
|
||||||
"IOTA_ASSET_DIR=${cfg.assetDir}"
|
|
||||||
"IOTA_DEPLOYMENT_MODE=system_socket_activated"
|
|
||||||
"IOTA_SUPERVISOR=systemd"
|
|
||||||
];
|
|
||||||
ExecStartPre = "+${pkgs.writeShellScript "iota-setup" ''
|
|
||||||
# ponytail: Preserve daemon-assigned IDs; remove config.yaml to reseed changed declarative settings.
|
|
||||||
if [ ! -e ${configFile} ]; then
|
|
||||||
install -m 0640 -o iota -g iota ${sourceConfigFile} ${configFile}
|
|
||||||
fi
|
|
||||||
${lib.optionalString (cfg.certFile != null) ''
|
|
||||||
install -d -m 0700 -o iota -g iota ${cfg.stateDir}/tls
|
|
||||||
install -m 0644 -o iota -g iota ${cfg.certFile} ${cfg.stateDir}/tls/cert.pem
|
|
||||||
install -m 0600 -o iota -g iota ${cfg.keyFile} ${cfg.stateDir}/tls/key.pem
|
|
||||||
''}
|
|
||||||
''}";
|
|
||||||
}
|
|
||||||
// lib.optionalAttrs (cfg.environmentFiles != [ ]) {
|
|
||||||
EnvironmentFile = cfg.environmentFiles;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
networking.firewall = lib.mkIf cfg.openFirewall {
|
|
||||||
allowedTCPPorts = [ cfg.port ];
|
|
||||||
allowedUDPPorts = [ cfg.port ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,5 @@ systemd/iota-daemon.service
|
||||||
systemd/iota-daemon.socket
|
systemd/iota-daemon.socket
|
||||||
systemd/sysusers.d/iota.conf
|
systemd/sysusers.d/iota.conf
|
||||||
systemd/iota-update.service
|
systemd/iota-update.service
|
||||||
systemd/iota-update.timer
|
|
||||||
systemd/update.env
|
systemd/update.env
|
||||||
manifest.json
|
manifest.json
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,14 @@ pub fn install_linux_bundle_with_operator(bundle: &Path, operator: Option<&str>)
|
||||||
let product_version = product_version(staging.path())?;
|
let product_version = product_version(staging.path())?;
|
||||||
validate_update_environment(staging.path())?;
|
validate_update_environment(staging.path())?;
|
||||||
|
|
||||||
|
for directory in ["/usr/local/lib/systemd/system", "/etc/systemd/system"] {
|
||||||
|
let timer = Path::new(directory).join("iota-update.timer");
|
||||||
|
if timer.exists() {
|
||||||
|
run("systemctl", &["disable", "--now", "iota-update.timer"])?;
|
||||||
|
fs::remove_file(timer).context("remove legacy unattended update timer")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render_daemon_service(&staging.path().join("systemd/iota-daemon.service"))?;
|
render_daemon_service(&staging.path().join("systemd/iota-daemon.service"))?;
|
||||||
|
|
||||||
let version_dir = format!(
|
let version_dir = format!(
|
||||||
|
|
@ -267,6 +275,9 @@ fn validate_update_environment(staging: &Path) -> Result<()> {
|
||||||
if entries.len() != 5 {
|
if entries.len() != 5 {
|
||||||
bail!("updater environment contains unexpected variables");
|
bail!("updater environment contains unexpected variables");
|
||||||
}
|
}
|
||||||
|
if !matches!(entries["IOTA_UPDATE_CHANNEL"], "stable" | "canary") {
|
||||||
|
bail!("update channel must be stable or canary");
|
||||||
|
}
|
||||||
let public_key = entries
|
let public_key = entries
|
||||||
.get("IOTA_UPDATE_PUBLIC_KEY")
|
.get("IOTA_UPDATE_PUBLIC_KEY")
|
||||||
.context("updater environment is missing IOTA_UPDATE_PUBLIC_KEY")?;
|
.context("updater environment is missing IOTA_UPDATE_PUBLIC_KEY")?;
|
||||||
|
|
@ -395,10 +406,10 @@ mod tests {
|
||||||
fn rejects_bundle_missing_contract_member() {
|
fn rejects_bundle_missing_contract_member() {
|
||||||
let directory = tempfile::tempdir().unwrap();
|
let directory = tempfile::tempdir().unwrap();
|
||||||
let bundle = directory.path().join("release.zip");
|
let bundle = directory.path().join("release.zip");
|
||||||
write_bundle(&bundle, Some("systemd/iota-update.timer"), "0.1.0");
|
write_bundle(&bundle, Some("systemd/iota-update.service"), "0.1.0");
|
||||||
|
|
||||||
let error = validate_linux_bundle(&bundle).unwrap_err();
|
let error = validate_linux_bundle(&bundle).unwrap_err();
|
||||||
assert!(error.to_string().contains("systemd/iota-update.timer"));
|
assert!(error.to_string().contains("systemd/iota-update.service"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
83
iota-updater/src/config.rs
Normal file
83
iota-updater/src/config.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
use anyhow::{Context, Result, bail};
|
||||||
|
use std::{collections::BTreeMap, fs, io::Write, path::Path};
|
||||||
|
|
||||||
|
const CONFIG: &str = "/etc/iota/update.env";
|
||||||
|
const RELEASES: &str = "https://git.methanium.net/tensamin/prod-pins/releases/download";
|
||||||
|
|
||||||
|
pub fn manifest_url(channel: &str, architecture: &str) -> Result<String> {
|
||||||
|
if !matches!(channel, "stable" | "canary") {
|
||||||
|
bail!("update channel must be stable or canary");
|
||||||
|
}
|
||||||
|
if !matches!(architecture, "x86_64" | "aarch64") {
|
||||||
|
bail!("unsupported update architecture: {architecture}");
|
||||||
|
}
|
||||||
|
Ok(format!(
|
||||||
|
"{RELEASES}/{channel}/iota-update-linux-{architecture}.json"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn load() -> Result<BTreeMap<String, String>> {
|
||||||
|
let contents = fs::read_to_string(CONFIG).context("read /etc/iota/update.env")?;
|
||||||
|
let mut entries = BTreeMap::new();
|
||||||
|
for line in contents.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.is_empty() || line.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (name, value) = line.split_once('=').context("invalid update.env entry")?;
|
||||||
|
if value.is_empty() || value.chars().any(char::is_whitespace) {
|
||||||
|
bail!("invalid update.env value for {name}");
|
||||||
|
}
|
||||||
|
if entries.insert(name.to_owned(), value.to_owned()).is_some() {
|
||||||
|
bail!("duplicate update.env entry {name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name in [
|
||||||
|
"IOTA_UPDATE_CHANNEL",
|
||||||
|
"IOTA_UPDATE_PUBLIC_KEY",
|
||||||
|
"IOTA_UPDATE_SIGNING_KEY_ID",
|
||||||
|
] {
|
||||||
|
if !entries.contains_key(name) {
|
||||||
|
bail!("update.env is missing {name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let url = manifest_url(&entries["IOTA_UPDATE_CHANNEL"], std::env::consts::ARCH)?;
|
||||||
|
if entries.get("IOTA_UPDATE_MANIFEST") != Some(&url)
|
||||||
|
|| entries.get("IOTA_UPDATE_SIGNATURE") != Some(&format!("{url}.sig"))
|
||||||
|
{
|
||||||
|
bail!("update.env URLs do not match the selected prod-pins channel");
|
||||||
|
}
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected_channel() -> Result<String> {
|
||||||
|
Ok(load()?["IOTA_UPDATE_CHANNEL"].clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_channel(channel: &str) -> Result<()> {
|
||||||
|
let url = manifest_url(channel, std::env::consts::ARCH)?;
|
||||||
|
let paths = iota_paths::IotaPaths::resolve(iota_paths::Scope::System)
|
||||||
|
.map_err(|error| anyhow::anyhow!(error))?;
|
||||||
|
let transaction = crate::transaction::UpdateTransaction::from_paths(&paths)?;
|
||||||
|
let _lock = transaction.acquire()?;
|
||||||
|
let mut entries = load()?;
|
||||||
|
entries.insert("IOTA_UPDATE_CHANNEL".into(), channel.into());
|
||||||
|
entries.insert("IOTA_UPDATE_MANIFEST".into(), url.clone());
|
||||||
|
entries.insert("IOTA_UPDATE_SIGNATURE".into(), format!("{url}.sig"));
|
||||||
|
let path = Path::new(CONFIG);
|
||||||
|
let parent = path.parent().context("update.env has no parent")?;
|
||||||
|
let mut temporary = tempfile::NamedTempFile::new_in(parent)?;
|
||||||
|
temporary
|
||||||
|
.as_file()
|
||||||
|
.set_permissions(fs::metadata(path)?.permissions())?;
|
||||||
|
for (name, value) in entries {
|
||||||
|
writeln!(temporary, "{name}={value}")?;
|
||||||
|
}
|
||||||
|
temporary.as_file().sync_all()?;
|
||||||
|
temporary
|
||||||
|
.persist(path)
|
||||||
|
.map_err(|error| error.error)
|
||||||
|
.context("save update channel")?;
|
||||||
|
fs::File::open(parent)?.sync_all()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod config;
|
||||||
pub mod manifest;
|
pub mod manifest;
|
||||||
pub mod transaction;
|
pub mod transaction;
|
||||||
|
|
||||||
|
|
@ -80,9 +81,10 @@ struct UpdatePolicy {
|
||||||
|
|
||||||
impl UpdatePolicy {
|
impl UpdatePolicy {
|
||||||
fn from_environment() -> Result<Self> {
|
fn from_environment() -> Result<Self> {
|
||||||
|
let config = config::load()?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
channel: required_environment(CHANNEL_ENV)?,
|
channel: config[CHANNEL_ENV].clone(),
|
||||||
signing_key_id: required_environment(SIGNING_KEY_ID_ENV)?,
|
signing_key_id: config[SIGNING_KEY_ID_ENV].clone(),
|
||||||
activation: ActivationPolicy::from_environment()?,
|
activation: ActivationPolicy::from_environment()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -105,6 +107,8 @@ struct PendingActivation {
|
||||||
product_version: String,
|
product_version: String,
|
||||||
daemon_was_active: bool,
|
daemon_was_active: bool,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
existing_target: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
|
@ -172,21 +176,21 @@ impl DaemonSupervisor for SystemdSupervisor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_update() -> Result<bool> {
|
pub async fn check_update() -> Result<bool> {
|
||||||
|
let paths = iota_paths::IotaPaths::resolve(iota_paths::Scope::System)
|
||||||
|
.map_err(|error| anyhow::anyhow!(error))?;
|
||||||
|
let transaction = UpdateTransaction::from_paths(&paths)?;
|
||||||
|
let _lock = transaction.acquire()?;
|
||||||
|
let policy = UpdatePolicy::from_environment()?;
|
||||||
let Some(release) = configured_release().await? else {
|
let Some(release) = configured_release().await? else {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
};
|
};
|
||||||
let paths = iota_paths::IotaPaths::resolve(iota_paths::Scope::System)
|
|
||||||
.map_err(|error| anyhow::anyhow!(error))?;
|
|
||||||
let policy = UpdatePolicy::from_environment()?;
|
|
||||||
let transaction = UpdateTransaction::from_paths(&paths)?;
|
|
||||||
let _lock = transaction.acquire()?;
|
|
||||||
let state = load_or_initialize_update_state(&paths)?;
|
let state = load_or_initialize_update_state(&paths)?;
|
||||||
if state.pending_activation.is_some() {
|
if state.pending_activation.is_some() {
|
||||||
bail!("an interrupted update requires iota-updater apply recovery");
|
bail!("an interrupted update requires iota-updater apply recovery");
|
||||||
}
|
}
|
||||||
let decision = evaluate_candidate(
|
let decision = evaluate_candidate(
|
||||||
&release.manifest,
|
&release.manifest,
|
||||||
current_version(&paths.install_root)?.as_deref(),
|
current_channel_version(&paths.install_root, &policy.channel)?.as_deref(),
|
||||||
state.channels.get(&policy.channel),
|
state.channels.get(&policy.channel),
|
||||||
&policy.channel,
|
&policy.channel,
|
||||||
&policy.signing_key_id,
|
&policy.signing_key_id,
|
||||||
|
|
@ -214,6 +218,12 @@ async fn apply_update_with_observer(
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
let transaction = UpdateTransaction::from_paths(paths)?;
|
let transaction = UpdateTransaction::from_paths(paths)?;
|
||||||
let _lock = transaction.acquire()?;
|
let _lock = transaction.acquire()?;
|
||||||
|
let configured_policy = UpdatePolicy::from_environment()?;
|
||||||
|
if policy.channel != configured_policy.channel
|
||||||
|
|| policy.signing_key_id != configured_policy.signing_key_id
|
||||||
|
{
|
||||||
|
bail!("update configuration changed; retry apply");
|
||||||
|
}
|
||||||
let mut state = load_or_initialize_update_state(paths)?;
|
let mut state = load_or_initialize_update_state(paths)?;
|
||||||
recover_interrupted_activation(
|
recover_interrupted_activation(
|
||||||
paths,
|
paths,
|
||||||
|
|
@ -228,7 +238,7 @@ async fn apply_update_with_observer(
|
||||||
};
|
};
|
||||||
let decision = evaluate_candidate(
|
let decision = evaluate_candidate(
|
||||||
&release.manifest,
|
&release.manifest,
|
||||||
current_version(&paths.install_root)?.as_deref(),
|
current_channel_version(&paths.install_root, &policy.channel)?.as_deref(),
|
||||||
state.channels.get(&policy.channel),
|
state.channels.get(&policy.channel),
|
||||||
&policy.channel,
|
&policy.channel,
|
||||||
&policy.signing_key_id,
|
&policy.signing_key_id,
|
||||||
|
|
@ -281,7 +291,24 @@ async fn apply_update_with_observer(
|
||||||
UpdatePhase::ActivationStarted,
|
UpdatePhase::ActivationStarted,
|
||||||
Some(&release.manifest.product_version),
|
Some(&release.manifest.product_version),
|
||||||
);
|
);
|
||||||
let activation = match transaction.activate(&release.manifest.product_version) {
|
let activation = match if state
|
||||||
|
.pending_activation
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|pending| pending.existing_target)
|
||||||
|
{
|
||||||
|
let previous_target = transaction.current_target()?;
|
||||||
|
transaction
|
||||||
|
.rollback(&release.manifest.product_version)
|
||||||
|
.map(|()| Activation {
|
||||||
|
previous_target,
|
||||||
|
new_target: transaction
|
||||||
|
.root
|
||||||
|
.join("versions")
|
||||||
|
.join(&release.manifest.product_version),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
transaction.activate(&release.manifest.product_version)
|
||||||
|
} {
|
||||||
Ok(activation) => activation,
|
Ok(activation) => activation,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
remove_unselected_candidate(&state, &transaction.root)?;
|
remove_unselected_candidate(&state, &transaction.root)?;
|
||||||
|
|
@ -395,7 +422,7 @@ fn remove_unselected_candidate(state: &UpdateState, install_root: &Path) -> Resu
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
validate_pending_candidate(pending, install_root)?;
|
validate_pending_candidate(pending, install_root)?;
|
||||||
if pending.new_target.exists() {
|
if !pending.existing_target && pending.new_target.exists() {
|
||||||
fs::remove_dir_all(&pending.new_target).with_context(|| {
|
fs::remove_dir_all(&pending.new_target).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"remove unselected candidate release {}",
|
"remove unselected candidate release {}",
|
||||||
|
|
@ -430,8 +457,20 @@ fn begin_activation_state(
|
||||||
.root
|
.root
|
||||||
.join("versions")
|
.join("versions")
|
||||||
.join(&manifest.product_version);
|
.join(&manifest.product_version);
|
||||||
if new_target.exists() {
|
let existing_target = new_target.exists();
|
||||||
bail!("release version already exists: {}", new_target.display());
|
if existing_target {
|
||||||
|
let mut installed = read_installed_manifest(&new_target)?;
|
||||||
|
// Refreshing signed expiry does not change the installed release contents.
|
||||||
|
installed.published_at = manifest.published_at.clone();
|
||||||
|
installed.expires_at = manifest.expires_at.clone();
|
||||||
|
if manifest::canonical_bytes(&installed)? != manifest::canonical_bytes(manifest)? {
|
||||||
|
bail!("existing release version has different signed metadata");
|
||||||
|
}
|
||||||
|
for artifact in
|
||||||
|
select_host_artifacts(manifest, std::env::consts::OS, std::env::consts::ARCH)?
|
||||||
|
{
|
||||||
|
manifest::verify_artifact(&new_target.join(&artifact.path), &artifact)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let mut updated = state.clone();
|
let mut updated = state.clone();
|
||||||
updated.pending_activation = Some(PendingActivation {
|
updated.pending_activation = Some(PendingActivation {
|
||||||
|
|
@ -442,6 +481,7 @@ fn begin_activation_state(
|
||||||
product_version: manifest.product_version.clone(),
|
product_version: manifest.product_version.clone(),
|
||||||
daemon_was_active,
|
daemon_was_active,
|
||||||
selected: true,
|
selected: true,
|
||||||
|
existing_target,
|
||||||
});
|
});
|
||||||
save_update_state(&paths.update_status_file(), &updated)?;
|
save_update_state(&paths.update_status_file(), &updated)?;
|
||||||
*state = updated;
|
*state = updated;
|
||||||
|
|
@ -603,7 +643,11 @@ fn evaluate_candidate(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if manifest.release_sequence == state.highest_accepted_sequence {
|
if manifest.release_sequence == state.highest_accepted_sequence {
|
||||||
return Ok(CandidateDecision::NoUpdate);
|
return Ok(if current_version.is_none() {
|
||||||
|
CandidateDecision::Install
|
||||||
|
} else {
|
||||||
|
CandidateDecision::NoUpdate
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if current_version == Some(manifest.product_version.as_str()) {
|
if current_version == Some(manifest.product_version.as_str()) {
|
||||||
|
|
@ -754,14 +798,6 @@ fn record_failed_sequence(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn required_environment(name: &'static str) -> Result<String> {
|
|
||||||
let value = std::env::var(name).with_context(|| format!("{name} is required"))?;
|
|
||||||
if value.is_empty() {
|
|
||||||
bail!("{name} must not be empty");
|
|
||||||
}
|
|
||||||
Ok(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn environment_u64(name: &'static str, default: u64) -> Result<u64> {
|
fn environment_u64(name: &'static str, default: u64) -> Result<u64> {
|
||||||
let value = match std::env::var(name) {
|
let value = match std::env::var(name) {
|
||||||
Ok(value) => value
|
Ok(value) => value
|
||||||
|
|
@ -848,20 +884,12 @@ struct ConfiguredRelease {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn configured_release() -> Result<Option<ConfiguredRelease>> {
|
async fn configured_release() -> Result<Option<ConfiguredRelease>> {
|
||||||
let Some(manifest_location) = std::env::var_os(MANIFEST_ENV) else {
|
let config = config::load()?;
|
||||||
return Ok(None);
|
configured_release_from_locations(
|
||||||
};
|
&config[MANIFEST_ENV],
|
||||||
let manifest_location = manifest_location
|
&config[SIGNATURE_ENV],
|
||||||
.into_string()
|
&config[PUBLIC_KEY_ENV],
|
||||||
.map_err(|_| anyhow::anyhow!("{MANIFEST_ENV} must be valid UTF-8"))?;
|
)
|
||||||
if manifest_location.is_empty() {
|
|
||||||
bail!("{MANIFEST_ENV} must not be empty");
|
|
||||||
}
|
|
||||||
let signature_location =
|
|
||||||
std::env::var(SIGNATURE_ENV).unwrap_or_else(|_| format!("{manifest_location}.sig"));
|
|
||||||
let key_text = std::env::var(PUBLIC_KEY_ENV)
|
|
||||||
.with_context(|| format!("{PUBLIC_KEY_ENV} is required when {MANIFEST_ENV} is set"))?;
|
|
||||||
configured_release_from_locations(&manifest_location, &signature_location, &key_text)
|
|
||||||
.await
|
.await
|
||||||
.map(Some)
|
.map(Some)
|
||||||
}
|
}
|
||||||
|
|
@ -990,6 +1018,11 @@ fn current_version(install_root: &Path) -> Result<Option<String>> {
|
||||||
.map(str::to_owned))
|
.map(str::to_owned))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn current_channel_version(install_root: &Path, channel: &str) -> Result<Option<String>> {
|
||||||
|
let manifest = read_installed_manifest(&install_root.join("current"))?;
|
||||||
|
Ok((manifest.channel == channel).then_some(manifest.product_version))
|
||||||
|
}
|
||||||
|
|
||||||
fn read_installed_manifest(release_dir: &Path) -> Result<ReleaseManifest> {
|
fn read_installed_manifest(release_dir: &Path) -> Result<ReleaseManifest> {
|
||||||
let manifest_path = release_dir.join("manifest.json");
|
let manifest_path = release_dir.join("manifest.json");
|
||||||
let bytes = fs::read(&manifest_path).with_context(|| {
|
let bytes = fs::read(&manifest_path).with_context(|| {
|
||||||
|
|
@ -1507,6 +1540,7 @@ mod tests {
|
||||||
product_version: "1.1.0".into(),
|
product_version: "1.1.0".into(),
|
||||||
daemon_was_active: false,
|
daemon_was_active: false,
|
||||||
selected: true,
|
selected: true,
|
||||||
|
existing_target: false,
|
||||||
});
|
});
|
||||||
recover_interrupted_activation(
|
recover_interrupted_activation(
|
||||||
&paths,
|
&paths,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,15 @@ async fn main() -> Result<()> {
|
||||||
let command = std::env::args().nth(1).unwrap_or_else(|| "status".into());
|
let command = std::env::args().nth(1).unwrap_or_else(|| "status".into());
|
||||||
match command.as_str() {
|
match command.as_str() {
|
||||||
"check" => println!("{}", iota_updater::check_update().await?),
|
"check" => println!("{}", iota_updater::check_update().await?),
|
||||||
"status" => println!("updater ready"),
|
"status" => println!("channel: {}", iota_updater::config::selected_channel()?),
|
||||||
|
"channel" => {
|
||||||
|
if let Some(channel) = std::env::args().nth(2) {
|
||||||
|
iota_updater::config::select_channel(&channel)?;
|
||||||
|
println!("Selected {channel}. Run check, then apply to update.");
|
||||||
|
} else {
|
||||||
|
println!("{}", iota_updater::config::selected_channel()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
"apply" => {
|
"apply" => {
|
||||||
struct StderrObserver;
|
struct StderrObserver;
|
||||||
impl iota_updater::UpdateObserver for StderrObserver {
|
impl iota_updater::UpdateObserver for StderrObserver {
|
||||||
|
|
@ -27,7 +35,7 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"usage: iota-updater check|status|apply|rollback VERSION"
|
"usage: iota-updater check|status|apply|channel [stable|canary]|rollback VERSION"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -355,6 +355,11 @@ struct UpdateArgs {
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
enum UpdateAction {
|
enum UpdateAction {
|
||||||
Check,
|
Check,
|
||||||
|
Apply,
|
||||||
|
Channel {
|
||||||
|
#[arg(value_parser = ["stable", "canary"])]
|
||||||
|
channel: Option<String>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
struct CommunityArgs {
|
struct CommunityArgs {
|
||||||
|
|
@ -523,6 +528,10 @@ pub enum Command {
|
||||||
limit: usize,
|
limit: usize,
|
||||||
},
|
},
|
||||||
UpdateCheck,
|
UpdateCheck,
|
||||||
|
UpdateApply,
|
||||||
|
UpdateChannel {
|
||||||
|
channel: Option<String>,
|
||||||
|
},
|
||||||
CommunityList,
|
CommunityList,
|
||||||
TermsStatus {
|
TermsStatus {
|
||||||
system: bool,
|
system: bool,
|
||||||
|
|
@ -710,6 +719,8 @@ impl CliInvocation {
|
||||||
Some(CliCommand::Logs { limit }) => Command::Logs { limit },
|
Some(CliCommand::Logs { limit }) => Command::Logs { limit },
|
||||||
Some(CliCommand::Update(update)) => match update.action {
|
Some(CliCommand::Update(update)) => match update.action {
|
||||||
UpdateAction::Check => Command::UpdateCheck,
|
UpdateAction::Check => Command::UpdateCheck,
|
||||||
|
UpdateAction::Apply => Command::UpdateApply,
|
||||||
|
UpdateAction::Channel { channel } => Command::UpdateChannel { channel },
|
||||||
},
|
},
|
||||||
Some(CliCommand::Community(community)) => match community.action {
|
Some(CliCommand::Community(community)) => match community.action {
|
||||||
CommunityAction::List => Command::CommunityList,
|
CommunityAction::List => Command::CommunityList,
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,33 @@ async fn run() -> Result<(), StartupError> {
|
||||||
iota_installer::bootstrap_linux_bundle(Path::new(&bundle), operator.as_deref())
|
iota_installer::bootstrap_linux_bundle(Path::new(&bundle), operator.as_deref())
|
||||||
.map_err(|error| StartupError::Other(format!("Bootstrap failed: {error}")))
|
.map_err(|error| StartupError::Other(format!("Bootstrap failed: {error}")))
|
||||||
}
|
}
|
||||||
|
command @ (Command::UpdateCheck | Command::UpdateApply | Command::UpdateChannel { .. }) => {
|
||||||
|
let mut updater = std::process::Command::new(
|
||||||
|
iota_paths::current_version_link().join("bin/iota-updater"),
|
||||||
|
);
|
||||||
|
match command {
|
||||||
|
Command::UpdateCheck => {
|
||||||
|
updater.arg("check");
|
||||||
|
}
|
||||||
|
Command::UpdateApply => {
|
||||||
|
updater.arg("apply");
|
||||||
|
}
|
||||||
|
Command::UpdateChannel { channel } => {
|
||||||
|
updater.arg("channel");
|
||||||
|
if let Some(channel) = channel {
|
||||||
|
updater.arg(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
let status = updater
|
||||||
|
.status()
|
||||||
|
.map_err(|error| StartupError::Other(format!("Run updater: {error}")))?;
|
||||||
|
if !status.success() {
|
||||||
|
return Err(StartupError::Other(format!("Updater failed with {status}")));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
command => {
|
command => {
|
||||||
let endpoints = resolve_endpoints()?;
|
let endpoints = resolve_endpoints()?;
|
||||||
if matches!(
|
if matches!(
|
||||||
|
|
@ -406,6 +433,8 @@ fn print_help() {
|
||||||
println!(" components Show component health");
|
println!(" components Show component health");
|
||||||
println!(" logs [--limit N] Show recent log entries");
|
println!(" logs [--limit N] Show recent log entries");
|
||||||
println!(" update check Check for updates");
|
println!(" update check Check for updates");
|
||||||
|
println!(" update apply Apply the selected channel's signed update");
|
||||||
|
println!(" update channel [stable|canary] Show or select the update channel");
|
||||||
println!(" community list List communities");
|
println!(" community list List communities");
|
||||||
println!(" terms status Show terms acceptance status");
|
println!(" terms status Show terms acceptance status");
|
||||||
println!(" terms show <DOC> Show a terms document");
|
println!(" terms show <DOC> Show a terms document");
|
||||||
|
|
@ -717,6 +746,7 @@ async fn run_command(
|
||||||
Command::Components => LocalRequest::ListComponents,
|
Command::Components => LocalRequest::ListComponents,
|
||||||
Command::Logs { limit } => LocalRequest::GetLogs { limit },
|
Command::Logs { limit } => LocalRequest::GetLogs { limit },
|
||||||
Command::UpdateCheck => LocalRequest::CheckUpdate,
|
Command::UpdateCheck => LocalRequest::CheckUpdate,
|
||||||
|
Command::UpdateApply | Command::UpdateChannel { .. } => unreachable!(),
|
||||||
Command::CommunityList => LocalRequest::ListCommunities,
|
Command::CommunityList => LocalRequest::ListCommunities,
|
||||||
Command::UsersRelease {
|
Command::UsersRelease {
|
||||||
confirmed: false, ..
|
confirmed: false, ..
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,15 @@ contract="$repository_directory/iota-installer/bundle-files.txt"
|
||||||
staging_directory="$(mktemp -d)"
|
staging_directory="$(mktemp -d)"
|
||||||
trap 'rm -rf "$staging_directory"' EXIT
|
trap 'rm -rf "$staging_directory"' EXIT
|
||||||
|
|
||||||
|
case "$update_channel" in stable|canary) ;; *) echo "channel must be stable or canary" >&2; exit 2 ;; esac
|
||||||
|
architecture="$(jq -r '.artifacts[0].architecture' "$release_manifest")"
|
||||||
|
case "$architecture" in x86_64|aarch64) ;; *) echo "unsupported update architecture" >&2; exit 2 ;; esac
|
||||||
|
expected_url="https://git.methanium.net/tensamin/prod-pins/releases/download/$update_channel/iota-update-linux-$architecture.json"
|
||||||
|
if [[ "$update_manifest_url" != "$expected_url" || "$update_signature_url" != "$expected_url.sig" ]]; then
|
||||||
|
echo "update URLs must match the selected prod-pins channel" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ ! "$product_version" =~ ^[0-9A-Za-z][0-9A-Za-z.+_-]*$ ]]; then
|
if [[ ! "$product_version" =~ ^[0-9A-Za-z][0-9A-Za-z.+_-]*$ ]]; then
|
||||||
echo "product version contains unsupported characters: $product_version" >&2
|
echo "product version contains unsupported characters: $product_version" >&2
|
||||||
exit 2
|
exit 2
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,16 @@ staging_directory="$(mktemp -d)"
|
||||||
artifacts="$staging_directory/artifacts.jsonl"
|
artifacts="$staging_directory/artifacts.jsonl"
|
||||||
trap 'rm -rf "$staging_directory"' EXIT
|
trap 'rm -rf "$staging_directory"' EXIT
|
||||||
|
|
||||||
|
case "$channel" in stable|canary) ;; *) echo "channel must be stable or canary" >&2; exit 2 ;; esac
|
||||||
|
case "$operating_system/$architecture" in linux/x86_64|linux/aarch64) ;; *) echo "unsupported update platform" >&2; exit 2 ;; esac
|
||||||
|
source_sha="${IOTA_RELEASE_SOURCE_SHA:-$(git -C "$repository_directory" rev-parse HEAD)}"
|
||||||
|
if [[ ! "$source_sha" =~ ^[0-9a-f]{40}$ ]]; then
|
||||||
|
echo "IOTA_RELEASE_SOURCE_SHA must be a full source commit SHA" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
product_version="${product_version}-${channel}-${source_sha}"
|
||||||
|
signing_key_id="${IOTA_RELEASE_SIGNING_KEY_ID:-primary}"
|
||||||
|
|
||||||
if [[ ! "$release_sequence" =~ ^[1-9][0-9]*$ ]]; then
|
if [[ ! "$release_sequence" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
echo "release sequence must be a positive integer" >&2
|
echo "release sequence must be a positive integer" >&2
|
||||||
exit 2
|
exit 2
|
||||||
|
|
@ -42,7 +52,7 @@ while IFS=$'\t' read -r role artifact_path; do
|
||||||
--arg os "$operating_system" \
|
--arg os "$operating_system" \
|
||||||
--arg architecture "$architecture" \
|
--arg architecture "$architecture" \
|
||||||
--arg path "$artifact_path" \
|
--arg path "$artifact_path" \
|
||||||
--arg url "$base_url/$asset_name" \
|
--arg url "$base_url/$asset_name-$operating_system-$architecture" \
|
||||||
--arg sha256 "$(sha256sum "$source_path" | cut -d ' ' -f 1)" \
|
--arg sha256 "$(sha256sum "$source_path" | cut -d ' ' -f 1)" \
|
||||||
--argjson size "$(stat -c %s "$source_path")" \
|
--argjson size "$(stat -c %s "$source_path")" \
|
||||||
'{role: $role, os: $os, architecture: $architecture, path: $path, url: $url, sha256: $sha256, size: $size}' \
|
'{role: $role, os: $os, architecture: $architecture, path: $path, url: $url, sha256: $sha256, size: $size}' \
|
||||||
|
|
@ -56,7 +66,7 @@ jq -s \
|
||||||
--argjson release_sequence "$release_sequence" \
|
--argjson release_sequence "$release_sequence" \
|
||||||
--arg published_at "$published_at" \
|
--arg published_at "$published_at" \
|
||||||
--arg expires_at "$expires_at" \
|
--arg expires_at "$expires_at" \
|
||||||
--arg release_signing_key_id "primary" \
|
--arg release_signing_key_id "$signing_key_id" \
|
||||||
'{
|
'{
|
||||||
product_version: $product_version,
|
product_version: $product_version,
|
||||||
channel: $channel,
|
channel: $channel,
|
||||||
|
|
|
||||||
84
scripts/releases.md
Normal file
84
scripts/releases.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
# Unmanaged signed releases
|
||||||
|
|
||||||
|
Publishing belongs to prod-pins. Iota only builds and verifies release inputs.
|
||||||
|
|
||||||
|
## Operator commands
|
||||||
|
|
||||||
|
Run as root for the unmanaged system installation:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
iota update channel stable
|
||||||
|
iota update channel canary
|
||||||
|
iota update check
|
||||||
|
iota update apply
|
||||||
|
```
|
||||||
|
|
||||||
|
`iota update channel` shows the selected channel. Selection atomically persists
|
||||||
|
`/etc/iota/update.env` without fetching or applying a release. The standalone
|
||||||
|
`iota-updater` supports the same commands. Check and apply read this file directly,
|
||||||
|
including the pinned public key and key ID. Process environment cannot replace
|
||||||
|
the trust configuration. Channel selection preserves both trust values and all
|
||||||
|
per-channel anti-replay state.
|
||||||
|
|
||||||
|
The manifest URL is
|
||||||
|
`https://git.methanium.net/tensamin/prod-pins/releases/download/CHANNEL/iota-update-linux-ARCH.json`.
|
||||||
|
The signature URL appends `.sig`. CHANNEL is `stable` or `canary`, ARCH is
|
||||||
|
`x86_64` or `aarch64`. Apply can select a previously accepted target when the
|
||||||
|
active release belongs to the other channel. It still rejects older sequences,
|
||||||
|
sequence/version reuse and failed activation sequences. Same-channel explicit
|
||||||
|
rollback remains respected.
|
||||||
|
|
||||||
|
The initial ZIP installer remains supported. It preserves existing update.env
|
||||||
|
and removes a previously provisioned unattended update timer. The update service
|
||||||
|
is an explicit oneshot apply operation with no timer.
|
||||||
|
|
||||||
|
## Release script contracts
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bash scripts/build-update-manifest.sh BINARY_DIRECTORY BASE_VERSION CHANNEL RELEASE_SEQUENCE PUBLISHED_AT EXPIRES_AT linux ARCH BASE_URL OUTPUT.json
|
||||||
|
iota-release sign OUTPUT.json OUTPUT.json.sig
|
||||||
|
bash scripts/build-release-bundle.sh BINARY_DIRECTORY PRODUCT_VERSION OUTPUT.json MANIFEST_URL PUBLIC_KEY SIGNATURE_URL CHANNEL SIGNING_KEY_ID OUTPUT.zip
|
||||||
|
iota-bundle OUTPUT.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
The binary directory contains `iota`, `iota-daemon` and `iota-updater`.
|
||||||
|
The manifest script emits PRODUCT_VERSION as `BASE_VERSION-CHANNEL-FULL_SOURCE_SHA`.
|
||||||
|
Read `.product_version` from the generated manifest for the bundle command.
|
||||||
|
Use an immutable release BASE_URL in prod-pins. Upload the binaries there as
|
||||||
|
`iota-linux-ARCH`, `iota-daemon-linux-ARCH` and `iota-updater-linux-ARCH`.
|
||||||
|
The signed artifact paths remain `bin/iota`, `bin/iota-daemon` and
|
||||||
|
`bin/iota-updater`, with SHA-256 hashes and byte lengths.
|
||||||
|
|
||||||
|
Publish the manifest and signature under each mutable `stable` or `canary`
|
||||||
|
release as `iota-update-linux-ARCH.json` and `iota-update-linux-ARCH.json.sig`.
|
||||||
|
Sequences must increase per channel. Serialize publication per channel and
|
||||||
|
reject a lower sequence or reuse with a different product version. Keep
|
||||||
|
immutable binary assets available for already published manifests.
|
||||||
|
|
||||||
|
## Signature and workflow inputs
|
||||||
|
|
||||||
|
`IOTA_RELEASE_SIGNING_KEY` is a secret containing a 32-byte Ed25519 seed as
|
||||||
|
64 hexadecimal characters. `iota-release sign` prints the derived 32-byte
|
||||||
|
public key as hex. Compare it with the expected pinned public key before
|
||||||
|
publishing or building installer trust configuration.
|
||||||
|
|
||||||
|
The detached signature is 64 Ed25519 bytes encoded as 128 hexadecimal
|
||||||
|
characters. It signs `serde_json::to_vec` of the typed `ReleaseManifest`, in
|
||||||
|
the Rust declaration field order, with nested artifacts in their declaration
|
||||||
|
field order. It does not sign the pretty-printed JSON file bytes. Use the Rust
|
||||||
|
signer rather than a generic JSON canonicalizer.
|
||||||
|
|
||||||
|
The prod-pins workflow needs:
|
||||||
|
|
||||||
|
- `IOTA_RELEASE_SIGNING_KEY`, secret signing seed.
|
||||||
|
- `IOTA_RELEASE_SOURCE_SHA`, full 40-character Iota source commit SHA. Defaults
|
||||||
|
to the local repository HEAD, so set it explicitly when building elsewhere.
|
||||||
|
- `IOTA_RELEASE_SIGNING_KEY_ID`, defaults to `primary`.
|
||||||
|
- Expected pinned public key, supplied as the bundle PUBLIC_KEY argument.
|
||||||
|
- Base product version, channel, positive release sequence, RFC 3339 publication
|
||||||
|
and expiry timestamps, architecture and immutable artifact BASE_URL.
|
||||||
|
- A Forgejo API token with release upload access to `tensamin/prod-pins`.
|
||||||
|
|
||||||
|
Build the Rust release binaries plus `iota-release` and `iota-bundle`. Shell
|
||||||
|
scripts require bash, git, coreutils, jq and zip. No Iota publishing workflow
|
||||||
|
remains; verification runs in `.forgejo/workflows/validate.yml`.
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=Check for Tensamin Iota updates daily
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnBootSec=15min
|
|
||||||
OnUnitActiveSec=24h
|
|
||||||
RandomizedDelaySec=1h
|
|
||||||
Persistent=true
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
Loading…
Reference in a new issue