diff --git a/.github/workflows/build-and-upload.yml b/.github/workflows/build-and-upload.yml new file mode 100644 index 00000000..1ab864d8 --- /dev/null +++ b/.github/workflows/build-and-upload.yml @@ -0,0 +1,113 @@ +name: Build and upload AMI +on: + workflow_call: + inputs: + system: + type: string + runs-on: + type: string + release: + type: string +jobs: + build: + name: Build + runs-on: ${{ inputs.runs-on }} + permissions: + contents: read + id-token: write + attestations: write + outputs: + name: ${{ steps.build.outputs.name }} + digest: ${{ steps.upload-artifact.outputs.artifact-digest }} + env: + flakeref: .?dir=amis#hydraJobs.${{ inputs.release }}.amazonImage.${{ inputs.system }} + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: DeterminateSystems/nix-installer-action@e50d5f73bfe71c2dd0aa4218de8f4afa59f8f81d # v16 + with: + # HACK: lets lie that we support kvm. make-disk-image.nix is fast enough in emulation mode + # and aarch64 has no kvm on github actions + extra-conf: extra-system-features = kvm + - run: | + out=$(nix build -L "$flakeref" --print-out-paths) + echo "out=$out" >> "$GITHUB_OUTPUT" + echo "name=$(basename "$out")" >> "$GITHUB_OUTPUT" + id: build + env: + flakeref: .#hydraJobs.${{ inputs.release }}.amazonImage.${{ inputs.system }} + - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4 + id: upload-artifact + with: + name: ${{ steps.build.outputs.name }} + path: ${{ steps.build.outputs.out }} + # TODO: use https://github.com/arianvp/nix-attest to store more provenance information + - uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3 + if: github.ref == 'refs/heads/main' + with: + subject-name: ${{ steps.build.outputs.name }} + subject-digest: sha256:${{ steps.upload-artifact.outputs.artifact-digest }} + upload: + name: Upload + runs-on: ubuntu-latest + needs: [build] + environment: images + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + id: download-artifact + with: + name: ${{ needs.build.outputs.name }} + path: ./result + - uses: DeterminateSystems/nix-installer-action@e50d5f73bfe71c2dd0aa4218de8f4afa59f8f81d # v16 + - uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0 + with: + role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/upload-ami + aws-region: ${{ vars.AWS_REGION }} + + - name: Upload smoke test + id: upload-smoke-test + run: | + predicate=$(nix run .#upload-ami -- --image-info "$image_info" --prefix "nixos/" --s3-bucket "$images_bucket") + echo "predicate=$predicate" >> "$GITHUB_OUTPUT" + env: + image_info: ./result/nix-support/image-info.json + images_bucket: ${{ vars.IMAGES_BUCKET }} + + - name: Run smoke test + id: smoke-test + run: nix run .#smoke-test -- --image-id "$image_id" + env: + image_id: ${{ fromJson(steps.upload-smoke-test.outputs.predicate).image_ids[vars.AWS_REGION] }} + + - name: Clean up smoke test + if: ${{ cancelled() }} + run: | + image_id=$(echo "$image_ids" | jq -r '.[$ENV.AWS_REGION]') + nix run .#smoke-test -- --image-id "$image_id" --cancel + env: + image_ids: ${{ steps.upload-smoke-test.outputs.image_ids }} + + - name: Upload AMIs to all available regions + if: github.ref == 'refs/heads/main' + id: upload-amis + run: | + predicate=$(nix run .#upload-ami -- \ + --image-info "$image_info" \ + --prefix "nixos/" \ + --s3-bucket "$images_bucket" \ + --copy-to-regions \ + --public) + echo "predicate=$predicate" >> "GITHUB_OUTPUT" + env: + image_info: ./result/nix-support/image-info.json + images_bucket: ${{ vars.IMAGES_BUCKET }} + + # TODO: Only create if something was *actually* uploaded + - name: Create upload attestation + uses: actions/attest@a63cfcc7d1aab266ee064c58250cfc2c7d07bc31 # v2.1.1 + if: github.ref == 'refs/heads/main' + with: + subject-name: ${{ needs.build.outputs.name }} + subject-digest: sha256:${{ needs.build.outputs.digest }} + predicate-type: "https://github.com/NixOS/amis/predicates/upload-ami/v0" + predicate: ${{ steps.upload-amis.outputs.predicate }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..e24671be --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,83 @@ +name: Build and upload AMIs +on: + pull_request: + paths: + - amis/** + push: + branches: + - main + paths: + - amis/** +jobs: + build-and-upload: + name: ${{ matrix.release }} ${{ matrix.system.system }} + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.release }}-${{ matrix.system.runs-on }}-${{ matrix.system.system }} + cancel-in-progress: true + permissions: + id-token: write + attestations: write + contents: read + strategy: + fail-fast: false + matrix: + release: + # - nixos_2411 + - nixos_unstable + system: + - runs-on: ubuntu-latest + system: x86_64-linux + - runs-on: ubuntu-24.04-arm + system: aarch64-linux + uses: ./.github/workflows/build-and-upload.yml + with: + runs-on: ${{ matrix.system.runs-on }} + system: ${{ matrix.system.system }} + release: ${{ matrix.release }} + delete-deprecated-images: + name: Delete deprecated images + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + needs: build-and-upload + environment: images + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: DeterminateSystems/nix-installer-action@e50d5f73bfe71c2dd0aa4218de8f4afa59f8f81d # v16 + - uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0 + with: + role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/upload-ami + aws-region: ${{ vars.AWS_REGION }} + - name: Delete deprecated AMIs + if: github.ref == 'refs/heads/main' + run: "nix run .#delete-deprecated-images" + deploy-pages: + name: Deploy images page + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + needs: [build-and-upload, delete-deprecated-images] + permissions: + contents: read + id-token: write + pages: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: DeterminateSystems/nix-installer-action@e50d5f73bfe71c2dd0aa4218de8f4afa59f8f81d # v16 + - uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0 + with: + role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/github-pages + aws-region: ${{ vars.AWS_REGION }} + - name: Describe images + run: nix run .#describe-images > ./site/images.json + - name: Upload pages + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 + with: + path: ./site + - name: Deploy pages + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 + id: deployment diff --git a/.github/workflows/upload-legacy-ami.yml b/.github/workflows/upload-legacy-ami.yml deleted file mode 100644 index ef6085ec..00000000 --- a/.github/workflows/upload-legacy-ami.yml +++ /dev/null @@ -1,131 +0,0 @@ -name: Upload Legacy Amazon Image -permissions: - contents: read -on: - pull_request: - workflow_dispatch: - schedule: - - cron: "0 0 * * 0" -jobs: - upload-ami: - name: Upload Legacy Amazon Image - runs-on: ubuntu-latest - environment: images - permissions: - contents: read - id-token: write - strategy: - matrix: - release: - - release-24.11 - # - nixos-unstable - system: - - x86_64-linux - - aarch64-linux - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: DeterminateSystems/nix-installer-action@7993355175c2765e5733dae74f3e0786fe0e5c4f # v12 - - uses: DeterminateSystems/magic-nix-cache-action@87b14cf437d03d37989d87f0fa5ce4f5dc1a330b # v8 - # NOTE: We download the AMI from Hydra instead of building it ourselves - # because aarch64 is currently not supported by AWS EC2 and the legacy - # image builder requires nested virtualization. - - name: Download AMI from Hydra - id: download_ami - run: | - set -o pipefail - build_id=$(curl -sSfL -H 'Accept: application/json' https://hydra.nixos.org/job/nixos/${{ matrix.release }}/tested/latest-finished | jq -r '.id') - out=$(curl -sSfL -H 'Accept: application/json' "https://hydra.nixos.org/build/${build_id}/constituents" | jq -r '.[] | select(.job == "nixos.amazonImage.${{ matrix.system }}") | .buildoutputs.out.path') - nix-store --realise "$out" --add-root ./result - echo "image_info=$out/nix-support/image-info.json" >> "$GITHUB_OUTPUT" - - uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0 - with: - role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/upload-ami - aws-region: ${{ vars.AWS_REGION }} - - name: Upload Smoke test AMI - id: upload_smoke_test_ami - run: | - image_info='${{ steps.download_ami.outputs.image_info }}' - images_bucket='${{ vars.IMAGES_BUCKET }}' - image_ids=$(nix run .#upload-ami -- \ - --image-info "$image_info" \ - --prefix "smoketest/" \ - --s3-bucket "$images_bucket") - echo "image_ids=$image_ids" >> "$GITHUB_OUTPUT" - - name: Smoke test - id: smoke_test - # NOTE: make sure smoke test isn't cancelled. Such that instance gets cleaned up. - run: | - image_ids='${{ steps.upload_smoke_test_ami.outputs.image_ids }}' - image_id=$(echo "$image_ids" | jq -r '.["${{ vars.AWS_REGION }}"]') - nix run .#smoke-test -- --image-id "$image_id" - - name: Clean up smoke test - if: ${{ cancelled() }} - run: | - image_ids='${{ steps.upload_smoke_test_ami.outputs.image_ids }}' - image_id=$(echo "$image_ids" | jq -r '.["${{ vars.AWS_REGION }}"]') - nix run .#smoke-test -- --image-id "$image_id" --cancel - # NOTE: We do not pass run-id as we're not building the image ourselves - # and we thus need to poll hydra periodically. Including the run-id would - # cause us to register the same snapshot as an image over and over again - # for each run. - - name: Upload AMIs to all available regions - if: github.ref == 'refs/heads/main' - run: | - image_info='${{ steps.download_ami.outputs.image_info }}' - images_bucket='${{ vars.IMAGES_BUCKET }}' - nix run .#upload-ami -- \ - --image-info "$image_info" \ - --prefix "nixos/" \ - --s3-bucket "$images_bucket" \ - --copy-to-regions \ - --public - delete-deprecated-images: - name: Delete deprecated images - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - needs: upload-ami - environment: images - permissions: - contents: read - id-token: write - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: DeterminateSystems/nix-installer-action@7993355175c2765e5733dae74f3e0786fe0e5c4f # v12 - - uses: DeterminateSystems/magic-nix-cache-action@87b14cf437d03d37989d87f0fa5ce4f5dc1a330b # v8 - - uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0 - with: - role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/upload-ami - aws-region: ${{ vars.AWS_REGION }} - - name: Delete deprecated AMIs - if: github.ref == 'refs/heads/main' - run: "nix run .#delete-deprecated-images \n" - deploy-pages: - name: Deploy images page - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - needs: [upload-ami, delete-deprecated-images] - permissions: - contents: read - id-token: write - pages: write - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: DeterminateSystems/nix-installer-action@7993355175c2765e5733dae74f3e0786fe0e5c4f # v12 - - uses: DeterminateSystems/magic-nix-cache-action@87b14cf437d03d37989d87f0fa5ce4f5dc1a330b # v8 - - uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0 - with: - role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/github-pages - aws-region: ${{ vars.AWS_REGION }} - - name: Describe images - run: nix run .#describe-images > ./site/images.json - - name: Upload pages - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 - with: - path: ./site - - name: Deploy pages - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 - id: deployment - if: github.ref == 'refs/heads/main' diff --git a/amis/flake.lock b/amis/flake.lock new file mode 100644 index 00000000..06e58d57 --- /dev/null +++ b/amis/flake.lock @@ -0,0 +1,40 @@ +{ + "nodes": { + "nixos_2411": { + "flake": false, + "locked": { + "lastModified": 1745379839, + "narHash": "sha256-4i4BgNmFmWXlDuGnGV9lYxak+48cXP9BUDV2z/KpmRs=", + "rev": "9684b53175fc6c09581e94cc85f05ab77464c7e3", + "type": "tarball", + "url": "https://releases.nixos.org/nixos/24.11/nixos-24.11.717196.9684b53175fc/nixexprs.tar.xz" + }, + "original": { + "type": "tarball", + "url": "https://channels.nixos.org/nixos-24.11/nixexprs.tar.xz" + } + }, + "nixos_unstable": { + "flake": false, + "locked": { + "lastModified": 1745351412, + "narHash": "sha256-HQ4k20o3kwWKIMJMMohl23kf3Qn4vZCSLPnbtzTXJig=", + "rev": "c11863f1e964833214b767f4a369c6e6a7aba141", + "type": "tarball", + "url": "https://releases.nixos.org/nixos/unstable/nixos-25.05pre787278.c11863f1e964/nixexprs.tar.xz" + }, + "original": { + "type": "tarball", + "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz" + } + }, + "root": { + "inputs": { + "nixos_2411": "nixos_2411", + "nixos_unstable": "nixos_unstable" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/amis/flake.nix b/amis/flake.nix new file mode 100644 index 00000000..4f8b896f --- /dev/null +++ b/amis/flake.nix @@ -0,0 +1,54 @@ +{ + inputs = { + # NOTE: We use the channel tarballs as they contain a .version and + # .version-suffix file with the naming convetions we want. The + # lib.trivial.version for flakes and git repos returns the wrong thing + nixos_2411 = { + url = "https://channels.nixos.org/nixos-24.11/nixexprs.tar.xz"; + flake = false; + }; + nixos_unstable = { + url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"; + flake = false; + }; + }; + + outputs = + inputs: + let + lib = import "${inputs.nixos_unstable}/lib"; + in + { + + hydraJobs = lib.genAttrs [ "nixos_2411" "nixos_unstable" ] ( + release: + let + nixpkgs = inputs.${release}; + # NOTE: we can not use nixpkgs.lib.nixosSystem as that uses + # an extended version of lib that overrides lib.trivial.version + # with something flake-specific which breaks the naming conventions + # for images. (e.g. pre for unstable, beta for 25.05, etc) + nixosSystem = args: import "${nixpkgs}/nixos/lib/eval-config.nix" ({ system = null; } // args); + in + { + amazonImage = lib.genAttrs [ "aarch64-linux" "x86_64-linux" ] ( + system: + (nixosSystem { + modules = [ + # TODO: use @phaer's new images interface + "${nixpkgs}/nixos/maintainers/scripts/ec2/amazon-image.nix" + ( + { config, ... }: + { + system.stateVersion = config.system.nixos.release; + virtualisation.diskSize = "auto"; + nixpkgs.hostPlatform = system; + } + ) + ]; + }).config.system.build.amazonImage + ); + } + ); + }; +} diff --git a/flake.lock b/flake.lock index 81816b8d..df6ffaab 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,33 @@ { "nodes": { + "nixos_2411": { + "flake": false, + "locked": { + "lastModified": 1745379839, + "narHash": "sha256-4i4BgNmFmWXlDuGnGV9lYxak+48cXP9BUDV2z/KpmRs=", + "rev": "9684b53175fc6c09581e94cc85f05ab77464c7e3", + "type": "tarball", + "url": "https://releases.nixos.org/nixos/24.11/nixos-24.11.717196.9684b53175fc/nixexprs.tar.xz" + }, + "original": { + "type": "tarball", + "url": "https://channels.nixos.org/nixos-24.11/nixexprs.tar.xz" + } + }, + "nixos_unstable": { + "flake": false, + "locked": { + "lastModified": 1745351412, + "narHash": "sha256-HQ4k20o3kwWKIMJMMohl23kf3Qn4vZCSLPnbtzTXJig=", + "rev": "c11863f1e964833214b767f4a369c6e6a7aba141", + "type": "tarball", + "url": "https://releases.nixos.org/nixos/unstable/nixos-25.05pre787278.c11863f1e964/nixexprs.tar.xz" + }, + "original": { + "type": "tarball", + "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz" + } + }, "nixpkgs": { "locked": { "lastModified": 1739357830, @@ -18,6 +46,8 @@ }, "root": { "inputs": { + "nixos_2411": "nixos_2411", + "nixos_unstable": "nixos_unstable", "nixpkgs": "nixpkgs", "treefmt-nix": "treefmt-nix" } diff --git a/flake.nix b/flake.nix index 628ac7e1..53f34250 100644 --- a/flake.nix +++ b/flake.nix @@ -3,6 +3,18 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-24.11"; + + # NOTE: We use the channel tarballs as they contain a .version and + # .version-suffix file with the naming convetions we want. The + # lib.trivial.version for flakes and git repos returns the wrong thing + nixos_2411 = { + url = "https://channels.nixos.org/nixos-24.11/nixexprs.tar.xz"; + flake = false; + }; + nixos_unstable = { + url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"; + flake = false; + }; treefmt-nix = { url = "github:numtide/treefmt-nix"; inputs.nixpkgs.follows = "nixpkgs"; @@ -10,7 +22,7 @@ }; outputs = - { + inputs@{ self, nixpkgs, treefmt-nix, @@ -69,5 +81,36 @@ devShells = genAttrs supportedSystems (system: { default = self.packages.${system}.upload-ami; }); + + hydraJobs = genAttrs [ "nixos_2411" "nixos_unstable" ] ( + release: + let + nixpkgs = inputs.${release}; + # NOTE: we can not use nixpkgs.lib.nixosSystem as that uses + # an extended version of lib that overrides lib.trivial.version + # with something flake-specific which breaks the naming conventions + # for images. (e.g. pre for unstable, beta for 25.05, etc) + nixosSystem = args: import "${nixpkgs}/nixos/lib/eval-config.nix" ({ system = null; } // args); + in + { + amazonImage = genAttrs [ "aarch64-linux" "x86_64-linux" ] ( + system: + (nixosSystem { + modules = [ + # TODO: use @phaer's new images interface + "${nixpkgs}/nixos/maintainers/scripts/ec2/amazon-image.nix" + ( + { config, ... }: + { + system.stateVersion = config.system.nixos.release; + virtualisation.diskSize = "auto"; + nixpkgs.hostPlatform = system; + } + ) + ]; + }).config.system.build.amazonImage + ); + } + ); }; } diff --git a/upload-ami/src/upload_ami/upload_ami.py b/upload-ami/src/upload_ami/upload_ami.py index f1fb447f..569e8f36 100644 --- a/upload-ami/src/upload_ami/upload_ami.py +++ b/upload-ami/src/upload_ami/upload_ami.py @@ -16,6 +16,7 @@ from mypy_boto3_s3.client import S3Client from concurrent.futures import ThreadPoolExecutor +from importlib import reload class ImageInfo(TypedDict): @@ -292,7 +293,7 @@ def _copy_image(target_region: RegionTypeDef) -> tuple[str, str]: def upload_ami( - image_info: ImageInfo, + image_info_file: str, s3_bucket: str, copy_to_regions: bool, prefix: str, @@ -308,11 +309,17 @@ def upload_ami( ec2: EC2Client = boto3.client("ec2") s3: S3Client = boto3.client("s3") - image_file = Path(image_info["file"]) + with open(image_info_file, "r") as f: + image_info = json.load(f) + + # HACK: This is to get this to work if we're pointing to an image-info + # out of the nix store + original_path = Path(image_info["file"]) + image_info_path = Path(image_info_file) + image_file = image_info_path.parent.parent / original_path.name label = image_info["label"] system = image_info["system"] image_name = prefix + label + "-" + system + ("." + run_id if run_id else "") - image_format = image_info.get("format") or "VHD" snapshot_id = import_snapshot_if_not_exist( s3, ec2, s3_bucket, image_name, image_file, image_format @@ -357,19 +364,30 @@ def main() -> None: level = logging.DEBUG if args.debug else logging.INFO logging.basicConfig(level=level) - with open(args.image_info, "r") as f: - image_info = json.load(f) - image_ids = {} image_ids = upload_ami( - image_info, + args.image_info, args.s3_bucket, args.copy_to_regions, args.prefix, args.run_id, args.public, ) - print(json.dumps(image_ids)) + + caller_identity = boto3.client("sts").get_caller_identity() + + with open(args.image_info, "r") as f: + image_info = json.load(f) + + print( + json.dumps( + { + "image_ids": image_ids, + "caller_identity": caller_identity, + "image_info": image_info, + } + ) + ) if __name__ == "__main__":