diff --git a/.fabrica.yaml b/.fabrica.yaml new file mode 100644 index 0000000..70861ab --- /dev/null +++ b/.fabrica.yaml @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: 2026 OpenCHAMI Contributors +# +# SPDX-License-Identifier: MIT + +project: + name: fru-tracker + module: github.com/example/fru-tracker + description: OpenCHAMI FRU tracker service with Fabrica-generated REST APIs for hardware discovery and inventory + created: 2026-02-26T11:09:26-08:00 +features: + validation: + enabled: true + mode: strict + events: + enabled: true + bus_type: memory + conditional: + enabled: true + etag_algorithm: sha256 + auth: + enabled: false + security: + authn: + enabled: false + authz: + enabled: false + mode: enforce + storage: + enabled: true + type: ent + metrics: + enabled: false +generation: + handlers: true + storage: true + client: true + openapi: true + events: true + middleware: true + reconciliation: true diff --git a/.github/workflows/PRBuild.yaml b/.github/workflows/PRBuild.yaml new file mode 100644 index 0000000..4db043c --- /dev/null +++ b/.github/workflows/PRBuild.yaml @@ -0,0 +1,107 @@ +# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# +# SPDX-License-Identifier: MIT + +name: Build each PR for testing and validation + +on: + pull_request: + branches: + - main + types: [opened, synchronize, reopened, edited] + workflow_dispatch: + inputs: + pr_number: + description: 'PR Number to build (optional, for manual PR builds)' + required: false + type: string + +permissions: write-all # Necessary for the generate-build-provenance action with containers + +jobs: + + build: + + + runs-on: ubuntu-latest + + steps: + - name: Set up latest stable Go + uses: actions/setup-go@v6.4.0 + with: + go-version: stable + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver-opts: | + image=moby/buildkit:master + network=host + - name: Docker Login + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout + uses: actions/checkout@v6.0.2 + with: + fetch-tags: 1 + fetch-depth: 0 + # Set environment variables required by GoReleaser + - name: Set build environment variables + run: | + echo "GIT_STATE=$(if git diff-index --quiet HEAD --; then echo 'clean'; else echo 'dirty'; fi)" >> $GITHUB_ENV + echo "BUILD_HOST=$(hostname)" >> $GITHUB_ENV + echo "GO_VERSION=$(go version | awk '{print $3}')" >> $GITHUB_ENV + echo "BUILD_USER=$(whoami)" >> $GITHUB_ENV + echo "CGO_ENABLED=0" >> $GITHUB_ENV + echo "IS_PR_BUILD=true" >> $GITHUB_ENV + + - name: Docker Login + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create Tag for PR + if: github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && inputs.pr_number != '') + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + PR_NUM="${{ github.event.number }}" + if [[ "${{ inputs.pr_number }}" != "" ]]; then + PR_NUM="${{ inputs.pr_number }}" + fi + git tag -f -a pr-${PR_NUM} -m "PR Release" + + - name: Build/Push container with goreleaser + uses: goreleaser/goreleaser-action@v6 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + version: '~> 2' + args: release --clean --skip=announce,validate,archive + id: goreleaser + - name: Process goreleaser output + id: process_goreleaser_output + run: | + echo "const fs = require('fs');" > process.js + echo 'const artifacts = ${{ steps.goreleaser.outputs.artifacts }}' >> process.js + echo "const firstNonNullDigest = artifacts.find(artifact => artifact.extra && artifact.extra.Digest != null)?.extra.Digest;" >> process.js + echo "console.log(firstNonNullDigest);" >> process.js + echo "fs.writeFileSync('digest.txt', firstNonNullDigest);" >> process.js + node process.js + echo "digest=$(cat digest.txt)" >> $GITHUB_OUTPUT + - name: Attest Binaries + uses: actions/attest-build-provenance@v4.1.0 + with: + subject-path: dist/**/fru-tracker* + - name: generate build provenance + uses: actions/attest-build-provenance@v4.1.0 + with: + subject-name: ghcr.io/openchami/fru-tracker + subject-digest: ${{ steps.process_goreleaser_output.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/REUSE.yaml b/.github/workflows/REUSE.yaml new file mode 100644 index 0000000..c42cb9a --- /dev/null +++ b/.github/workflows/REUSE.yaml @@ -0,0 +1,16 @@ +# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-FileCopyrightText: 2020 Free Software Foundation Europe e.V. +# +# SPDX-License-Identifier: CC0-1.0 +# SPDX-License-Identifier: MIT +name: REUSE Compliance Check + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6.0.2 + - name: REUSE Compliance Check + uses: fsfe/reuse-action@v6 diff --git a/.github/workflows/codegen-check.yaml b/.github/workflows/codegen-check.yaml new file mode 100644 index 0000000..580ee79 --- /dev/null +++ b/.github/workflows/codegen-check.yaml @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: 2026 OpenCHAMI Contributors +# +# SPDX-License-Identifier: MIT + +name: Codegen Check + +on: + pull_request: + branches: [main] + paths: + - "apis/**" + - "cmd/server/**" + - "internal/middleware/**" + - "internal/storage/**" + - "pkg/apiversion/**" + - "pkg/client/**" + - "pkg/reconcilers/**" + - "Makefile" + - ".github/workflows/codegen-check.yaml" + workflow_dispatch: + +jobs: + codegen-drift: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6.0.2 + + - name: Set up Go + uses: actions/setup-go@v6.4.0 + with: + go-version: stable + + - name: Verify generated code is committed + run: make generate-check diff --git a/.github/workflows/golangci-lint.yaml b/.github/workflows/golangci-lint.yaml new file mode 100644 index 0000000..b242ba3 --- /dev/null +++ b/.github/workflows/golangci-lint.yaml @@ -0,0 +1,25 @@ +# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# +# SPDX-License-Identifier: MIT +name: golangci-lint + +on: + push: + branches: [ main ] + pull_request: + workflow_dispatch: + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Set up latest stable Go + uses: actions/setup-go@v6.4.0 + with: + go-version: stable + - uses: actions/checkout@v6.0.2 + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v9.2.0 + with: + version: latest diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c338949..b407bbd 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,50 +1,83 @@ -# SPDX-FileCopyrightText: 2026 Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC # # SPDX-License-Identifier: MIT -name: Release +name: Release with goreleaser on: + workflow_dispatch: push: tags: - - 'v*.*.*' + - v* -permissions: - contents: write - packages: write +permissions: write-all # Necessary for the generate-build-provenance action with containers jobs: - goreleaser: + + build: + + runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Set up Go - uses: actions/setup-go@v5 + steps: + - name: Set up latest stable Go + uses: actions/setup-go@v6.4.0 with: - go-version: '1.23' - + go-version: stable - name: Set up QEMU uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry + with: + driver-opts: | + image=moby/buildkit:master + network=host + - name: Docker Login uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout + uses: actions/checkout@v6.0.2 + with: + fetch-tags: 1 + fetch-depth: 0 + # Set environment variables required by GoReleaser + - name: Set build environment variables + run: | + echo "GIT_STATE=$(if git diff-index --quiet HEAD --; then echo 'clean'; else echo 'dirty'; fi)" >> $GITHUB_ENV + echo "BUILD_HOST=$(hostname)" >> $GITHUB_ENV + echo "GO_VERSION=$(go version | awk '{print $3}')" >> $GITHUB_ENV + echo "BUILD_USER=$(whoami)" >> $GITHUB_ENV + echo "CGO_ENABLED=0" >> $GITHUB_ENV + echo "IS_PR_BUILD=false" >> $GITHUB_ENV - - name: Run GoReleaser + - name: Release with goreleaser uses: goreleaser/goreleaser-action@v6 + env: + GITHUB_TOKEN: ${{ github.token }} with: - distribution: goreleaser - version: '~> v2' + version: latest args: release --clean - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + id: goreleaser + - name: Process goreleaser output + id: process_goreleaser_output + run: | + echo "const fs = require('fs');" > process.js + echo 'const artifacts = ${{ steps.goreleaser.outputs.artifacts }}' >> process.js + echo "const firstNonNullDigest = artifacts.find(artifact => artifact.extra && artifact.extra.Digest != null)?.extra.Digest;" >> process.js + echo "console.log(firstNonNullDigest);" >> process.js + echo "fs.writeFileSync('digest.txt', firstNonNullDigest);" >> process.js + node process.js + echo "digest=$(cat digest.txt)" >> $GITHUB_OUTPUT + - name: Attest Binaries + uses: actions/attest-build-provenance@v1 + with: + subject-path: dist/**/fru-tracker* + - name: generate build provenance + uses: actions/attest-build-provenance@v1 + with: + subject-name: ghcr.io/openchami/fru-tracker + subject-digest: ${{ steps.process_goreleaser_output.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..728428f --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,82 @@ +# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# +# SPDX-License-Identifier: MIT + +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '39 5 * * 1' + push: + branches: [ "main" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + # `publish_results: true` only works when run from the default branch. conditional can be removed if disabled. + if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request' + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # (Optional) Uncomment file_mode if you have a .gitattributes with files marked export-ignore + # file_mode: git + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..2e4247d --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,38 @@ +# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# +# SPDX-License-Identifier: MIT + +name: Stale issues/PRs (org default) + +on: + schedule: + - cron: "23 3 * * *" # daily at 03:23 UTC + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +jobs: + run-stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + days-before-stale: 35 + days-before-close: 7 + stale-issue-label: "stale" + stale-pr-label: "stale" + exempt-issue-labels: "pinned,security,help wanted" + exempt-pr-labels: "pinned,security" + operations-per-run: 500 + remove-stale-when-updated: true + stale-issue-message: > + This issue has been automatically marked as stale due to inactivity. + Please add a comment or label to keep it open; otherwise it will close in 7 days. + close-issue-message: > + Closing due to inactivity. Feel free to reopen with new information. + stale-pr-message: > + This PR has been marked stale due to inactivity. Comment or push changes to keep it open. + close-pr-message: > + Closing due to inactivity. Please reopen when it’s ready again. diff --git a/.gitignore b/.gitignore index cb4d0c8..fad46f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 OpenCHAMI Contributors +# +# SPDX-License-Identifier: MIT # Generated by Fabrica dev # Template: init/gitignore.tmpl @@ -29,6 +32,12 @@ data/ *.yml !example.yaml !example.yml +!apis.yaml +!.pre-commit-config.yaml +!.github/workflows/*.yaml +!.github/workflows/*.yml +!.fabrica.yaml + # IDE files .vscode/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 023b71e..8b8de36 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -114,4 +114,3 @@ release: footer: | --- **Full Changelog**: https://github.com/openchami/fru-tracker/compare/{{ .PreviousTag }}...{{ .Tag }} - \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..672a3de --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,83 @@ +# SPDX-FileCopyrightText: Copyright © 2025 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + +- repo: https://github.com/fsfe/reuse-tool + rev: v5.1.1 + hooks: + - id: reuse-lint-file + +- repo: https://github.com/tekwizely/pre-commit-golang + # See 'pre-commit help autoupdate' + rev: v1.0.0-rc.2 + hooks: + + # Disabled go-mod-tidy in favor of custom hook below that properly excludes examples + # - id: go-mod-tidy + # exclude: '^(test/integration|examples)/' + # Disabled go-test-mod in favor of custom hook below that excludes examples + # - id: go-test-mod + # exclude: '^(test/integration|examples)/' + # Disabled go-vet-mod in favor of custom hook below that excludes examples + # - id: go-vet-mod + # exclude: '^(test/integration|examples)/' + + # + # Formatters + # + - id: go-fmt-repo + args: ["-w"] + + # + # Style Checkers + # Disabled golangci-lint-mod in favor of custom hook below that excludes examples + # - id: golangci-lint-mod + # exclude: '^(test/integration|examples)/' + +# Custom local hooks +- repo: local + hooks: + - id: go-mod-tidy-exclude-examples + name: go mod tidy (excluding examples) + # Temporarily rename examples to hide it from go mod tidy, then restore it + entry: bash -c 'if [ -d examples ]; then mv examples .examples.tmp && trap "rm -rf examples; mv .examples.tmp examples" EXIT; go mod tidy; else go mod tidy; fi' + language: system + pass_filenames: false + types: [go] + files: '^(go\.(mod|sum)|cmd/|pkg/|internal/).*' + + - id: go-vet-mod-exclude-examples + name: go vet (excluding examples) + # Temporarily rename examples to hide it from go vet, then restore it + entry: bash -c 'if [ -d examples ]; then mv examples .examples.tmp && trap "rm -rf examples; mv .examples.tmp examples" EXIT; go vet -mod=mod ./...; else go vet -mod=mod ./...; fi' + language: system + pass_filenames: false + types: [go] + files: '^(cmd/|pkg/|internal/).*\.go$' + + - id: golangci-lint-mod-exclude-examples + name: golangci-lint (excluding examples) + # Temporarily rename examples to hide it from golangci-lint, then restore it + entry: bash -c 'if [ -d examples ]; then mv examples .examples.tmp && trap "rm -rf examples; mv .examples.tmp examples" EXIT; golangci-lint run --fix=false; else golangci-lint run --fix=false; fi' + language: system + pass_filenames: false + types: [go] + files: '^(cmd/|pkg/|internal/).*\.go$' + + - id: go-test-mod-exclude-examples + name: go test (excluding examples and integration tests) + entry: bash -c 'go test -mod=readonly -race $(go list ./... 2>/dev/null | grep -v -e "/examples/" -e "/test/integration")' + language: system + pass_filenames: false + types: [go] + + # diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSES/CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 0000000..d817195 --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..22d2180 --- /dev/null +++ b/Makefile @@ -0,0 +1,236 @@ +# Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-FileCopyrightText: 2026 OpenCHAMI Contributors +# +# SPDX-License-Identifier: MIT + +.PHONY: help build test test-integration lint clean install run docker-build docker-run release-test check-no-pkg-resources-imports generate generate-check dev reuse-annotate-generated + +# Variables +BINARY_NAME=fru-tracker-server +CLIENT_BINARY_NAME=fru-tracker-client +GO=go +GOFLAGS=-v +TEST_TIMEOUT ?= 5m +VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") +COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") +DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") +LDFLAGS=-ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE)" +FABRICA_CMD ?= go run github.com/openchami/fabrica/cmd/fabrica@latest +FABRICA_SOURCE_ARG ?= +FABRICA_ENV ?= +LOCAL_FABRICA ?= + +ifneq ($(strip $(LOCAL_FABRICA)),) +FABRICA_CMD := $(LOCAL_FABRICA)/bin/fabrica +FABRICA_SOURCE_ARG := --fabrica-source $(LOCAL_FABRICA) +FABRICA_ENV := GOTOOLCHAIN=auto +endif + +help: ## Display this help screen + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + + +build: generate + go build -o bin/$(BINARY_NAME) ./cmd/server/ + go build -o bin/$(CLIENT_BINARY_NAME) ./cmd/client/ + +generate: ## Regenerate Fabrica outputs +ifneq ($(strip $(LOCAL_FABRICA)),) + @if [ ! -x $(LOCAL_FABRICA)/bin/fabrica ]; then \ + echo "Local Fabrica binary not found at $(LOCAL_FABRICA)/bin/fabrica"; \ + echo "Build it with: (cd $(LOCAL_FABRICA) && go build -o bin/fabrica ./cmd/fabrica)"; \ + exit 1; \ + fi +endif + $(FABRICA_ENV) $(FABRICA_CMD) generate $(FABRICA_SOURCE_ARG) + $(GO) mod tidy + @$(MAKE) reuse-annotate-generated + +reuse-annotate-generated: ## Add SPDX headers to generated Go files when REUSE is installed + @if command -v reuse >/dev/null 2>&1; then \ + files=$$({ \ + find internal/storage/ent -type f -name '*.go' -print; \ + find cmd internal pkg -type f -name '*_generated.go' -print; \ + printf '%s\n' cmd/server/export.go cmd/server/import.go cmd/client/main.go; \ + } | while IFS= read -r file; do [ -f "$$file" ] && echo "$$file"; done | sort -u); \ + if [ -n "$$files" ]; then \ + reuse annotate --copyright="OpenCHAMI Contributors" --license="MIT" --year="$(shell date +%Y)" --skip-existing $$files; \ + fi; \ + else \ + echo "reuse not installed; skipping generated file annotation"; \ + fi + +generate-check: ## Fail if generated files are out of sync (requires clean git tree) + @if ! git diff --quiet || ! git diff --cached --quiet; then \ + echo "Working tree must be clean before running generate-check"; \ + git --no-pager status --short; \ + exit 1; \ + fi + $(MAKE) generate LOCAL_FABRICA="$(LOCAL_FABRICA)" + @if ! git diff --quiet || ! git diff --cached --quiet; then \ + echo "Generated files are out of sync. Run 'make generate' and commit the results."; \ + git --no-pager diff --stat; \ + exit 1; \ + fi + +dev: build ## Regenerate code then build server and client binaries + +test: ## Run tests + $(GO) test $(GOFLAGS) -timeout $(TEST_TIMEOUT) -race -coverprofile=coverage.out -covermode=atomic $$(go list ./... 2>/dev/null | grep -v /demo/) + +test-integration: ## Run integration tests with explicit timeout (override with TEST_TIMEOUT=) + $(GO) test $(GOFLAGS) -timeout $(TEST_TIMEOUT) ./cmd/server -run TestDiscoverySnapshotIntegration -v + +test-coverage: test ## Run tests with coverage report + $(GO) tool cover -html=coverage.out -o coverage.html + @echo "Coverage report generated: coverage.html" + +lint: ## Run golangci-lint + golangci-lint run + +lint-fix: ## Run golangci-lint with auto-fix + golangci-lint run --fix + +clean: ## Clean build artifacts + rm -rf bin/ dist/ coverage.out coverage.html + $(GO) clean -cache + +tidy: ## Tidy go.mod + $(GO) mod tidy + +run: build ## Build and run the application + ./bin/$(BINARY_NAME) serve + +docker-build: ## Build Docker image + docker build -t fru-tracker:latest . + +docker-run: docker-build ## Build and run Docker container + docker run --rm fru-tracker:latest + +release-snapshot: ## Create a snapshot release with GoReleaser + goreleaser release --snapshot --clean + +release-test: ## Test release locally using GoReleaser snapshot (requires goreleaser) + @command -v goreleaser >/dev/null 2>&1 || { echo "goreleaser is required but not installed. Install with: 'brew install goreleaser' or 'go install github.com/goreleaser/goreleaser@latest'"; exit 1; } + @goreleaser release --snapshot --clean + +fmt: ## Format code + $(GO) fmt ./... + goimports -w . + +vet: ## Run go vet + $(GO) vet ./... + +vuln: ## Check for vulnerabilities + govulncheck ./... + +reuse: ## Check REUSE compliance + reuse lint + +reuse-spdx: ## Generate SPDX bill of materials + reuse spdx -o reuse.spdx + +reuse-install: ## Install REUSE tool + @command -v pipx >/dev/null 2>&1 || { echo "pipx is required but not installed. Install it with: python3 -m pip install --user pipx"; exit 1; } + pipx install reuse + @echo "REUSE tool installed successfully" + +reuse-annotate: ## Add REUSE headers to all files in the repository + @echo "Annotating files with REUSE headers..." + @echo "This will add SPDX headers to files that don't have them yet." +# REUSE-IgnoreStart + @read -p "Copyright holder [OpenCHAMI Contributors]: " holder; \ + holder=$${holder:-OpenCHAMI Contributors}; \ + read -p "License [MIT]: " license; \ + license=$${license:-MIT}; \ + read -p "Year [$(shell date +%Y)]: " year; \ + year=$${year:-$(shell date +%Y)}; \ + echo "Annotating with: SPDX-FileCopyrightText: $$year $$holder"; \ + echo " SPDX-License-Identifier: $$license"; \ + reuse annotate --copyright="$$holder" --license="$$license" --year="$$year" --skip-existing --recursive --skip-unrecognized . +# REUSE-IgnoreEnd + +reuse-download-license: ## Download a license file (usage: make reuse-download-license LICENSE=MIT) + @if [ -z "$(LICENSE)" ]; then \ + echo "Error: LICENSE variable is required. Usage: make reuse-download-license LICENSE=MIT"; \ + exit 1; \ + fi + reuse download $(LICENSE) + +pre-commit-install: ## Install pre-commit tool + @command -v pipx >/dev/null 2>&1 || { echo "pipx is required but not installed. Install it with: python3 -m pip install --user pipx"; exit 1; } + pipx install pre-commit + @echo "pre-commit installed successfully" + +pre-commit-setup: ## Install pre-commit hooks + @command -v pre-commit >/dev/null 2>&1 || { echo "pre-commit is not installed. Run 'make pre-commit-install' first."; exit 1; } + pre-commit install + pre-commit install --hook-type commit-msg + @echo "pre-commit hooks installed successfully" + +pre-commit-run: ## Run pre-commit hooks on all files + pre-commit run --all-files + +pre-commit-update: ## Update pre-commit hooks to latest versions + pre-commit autoupdate + +setup-dev: reuse-install pre-commit-install pre-commit-setup ## Set up development environment (install tools and hooks) + @echo "" + @echo "Development environment setup complete!" + @echo "Next steps:" + @echo " 1. Run 'make reuse-annotate' to add REUSE headers to all files" + @echo " 2. Run 'make pre-commit-run' to test pre-commit hooks" + @echo " 3. Start coding! Pre-commit hooks will run automatically on git commit" + @echo "" + @echo "Optional: Install 'act' to test GitHub Actions locally:" + @echo " brew install act" + @echo " make act-list # List available workflows" + +act-install: ## Install act (GitHub Actions local runner) via Homebrew + @command -v brew >/dev/null 2>&1 || { echo "Homebrew is required. Install from https://brew.sh"; exit 1; } + brew install act + @echo "act installed successfully" + +act-list: ## List all GitHub Actions workflows + @command -v act >/dev/null 2>&1 || { echo "act is not installed. Run 'make act-install' first."; exit 1; } + @echo "Available workflows:" + @ls -1 .github/workflows/*.yaml | sed 's/.*\// - /' + +act-test: ## Run GitHub Actions test workflow locally (ubuntu only, stable Go) + @command -v act >/dev/null 2>&1 || { echo "act is not installed. Run 'make act-install' first."; exit 1; } + @echo "Note: Testing with ubuntu-latest and stable Go version only (full matrix runs on GitHub)" + act push -W .github/workflows/test.yaml --container-architecture linux/amd64 --matrix os:ubuntu-latest --matrix go-version:stable + +act-build: ## Run GitHub Actions build workflow locally + @command -v act >/dev/null 2>&1 || { echo "act is not installed. Run 'make act-install' first."; exit 1; } + act push -W .github/workflows/build.yaml --container-architecture linux/amd64 + +act-lint: ## Run GitHub Actions golangci-lint workflow locally + @command -v act >/dev/null 2>&1 || { echo "act is not installed. Run 'make act-install' first."; exit 1; } + act push -W .github/workflows/golangci-lint.yaml --container-architecture linux/amd64 + +act-reuse: ## Run GitHub Actions REUSE workflow locally + @command -v act >/dev/null 2>&1 || { echo "act is not installed. Run 'make act-install' first."; exit 1; } + act push -W .github/workflows/reuse.yaml --container-architecture linux/amd64 + +act-vuln: ## Run GitHub Actions vulnerability check workflow locally + @command -v act >/dev/null 2>&1 || { echo "act is not installed. Run 'make act-install' first."; exit 1; } + act push -W .github/workflows/govulncheck.yaml --container-architecture linux/amd64 + +act-all: ## Run all testable workflows locally (build, test, lint, reuse, vuln) + @command -v act >/dev/null 2>&1 || { echo "act is not installed. Run 'make act-install' first."; exit 1; } + @echo "Running all testable workflows..." + @echo "\n=== Build Workflow ===" + @act -W .github/workflows/build.yaml || true + @echo "\n=== Test Workflow ===" + @act -W .github/workflows/test.yaml || true + @echo "\n=== Lint Workflow ===" + @act -W .github/workflows/golangci-lint.yaml || true + @echo "\n=== REUSE Workflow ===" + @act -W .github/workflows/reuse.yaml || true + @echo "\n=== Vulnerability Check Workflow ===" + @act -W .github/workflows/govulncheck.yaml || true + +all: clean install lint test build ## Run all checks and build + +.DEFAULT_GOAL := help diff --git a/README.md b/README.md index d564346..10e71f5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ + + # fru-tracker YouTube Video Demo: https://www.youtube.com/watch?v=TemS6Ok1c3c @@ -159,4 +165,4 @@ The API provides a bulk endpoint via the `DiscoverySnapshot` resource. Wrap your ] } } -``` \ No newline at end of file +``` diff --git a/apis.yaml b/apis.yaml new file mode 100644 index 0000000..7db38d5 --- /dev/null +++ b/apis.yaml @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2026 OpenCHAMI Contributors +# +# SPDX-License-Identifier: MIT + +groups: + - name: example.fabrica.dev + storageVersion: v1 + versions: + - v1 + resources: + - Device + - DiscoverySnapshot diff --git a/apis/example.fabrica.dev/v1/device_types.go b/apis/example.fabrica.dev/v1/device_types.go index 813f859..76b8df6 100644 --- a/apis/example.fabrica.dev/v1/device_types.go +++ b/apis/example.fabrica.dev/v1/device_types.go @@ -6,8 +6,8 @@ package v1 import ( "context" + "encoding/json" "github.com/openchami/fabrica/pkg/fabrica" - "encoding/json" ) // Device represents a device resource @@ -15,8 +15,8 @@ type Device struct { APIVersion string `json:"apiVersion"` Kind string `json:"kind"` Metadata fabrica.Metadata `json:"metadata"` - Spec DeviceSpec `json:"spec" validate:"required"` - Status DeviceStatus `json:"status,omitempty"` + Spec DeviceSpec `json:"spec" validate:"required"` + Status DeviceStatus `json:"status,omitempty"` } // DeviceSpec defines the desired state of Device @@ -52,6 +52,7 @@ func (r *Device) Validate(ctx context.Context) error { return nil } + // GetKind returns the kind of the resource func (r *Device) GetKind() string { return "Device" diff --git a/apis/example.fabrica.dev/v1/discoverysnapshot_types.go b/apis/example.fabrica.dev/v1/discoverysnapshot_types.go index 577fc12..478b01e 100644 --- a/apis/example.fabrica.dev/v1/discoverysnapshot_types.go +++ b/apis/example.fabrica.dev/v1/discoverysnapshot_types.go @@ -6,15 +6,15 @@ package v1 import ( "context" + "encoding/json" "github.com/openchami/fabrica/pkg/fabrica" - "encoding/json" ) // DiscoverySnapshot represents a discoverysnapshot resource type DiscoverySnapshot struct { - APIVersion string `json:"apiVersion"` - Kind string `json:"kind"` - Metadata fabrica.Metadata `json:"metadata"` + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Metadata fabrica.Metadata `json:"metadata"` Spec DiscoverySnapshotSpec `json:"spec" validate:"required"` Status DiscoverySnapshotStatus `json:"status,omitempty"` } @@ -43,6 +43,7 @@ func (r *DiscoverySnapshot) Validate(ctx context.Context) error { return nil } + // GetKind returns the kind of the resource func (r *DiscoverySnapshot) GetKind() string { return "DiscoverySnapshot" diff --git a/cmd/client/main.go b/cmd/client/main.go index 6c84fe1..2316c4a 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -1,5 +1,5 @@ -// Code generated by codegen. DO NOT EDIT. -// Copyright © 2025 OpenCHAMI a Series of LF Projects, LLC +// Code generated by Fabrica. DO NOT EDIT. +// Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // // SPDX-License-Identifier: MIT // @@ -21,6 +21,7 @@ // --timeout Request timeout (env: FRU_TRACKER_TIMEOUT) // --output, -o Output format: table, json, yaml (env: FRU_TRACKER_OUTPUT) // --version, -v API version to request: v1, v2beta1, etc. (env: FRU_TRACKER_VERSION) +// --token JWT bearer token (env: FRU_TRACKER_TOKEN) // --config Config file path (default: ~/.fru_tracker-cli.yaml) // // Configuration sources (in order of precedence): @@ -80,11 +81,12 @@ import ( ) var ( - cfgFile string - serverURL string - timeout time.Duration - output string - apiVersion string + cfgFile string + serverURL string + timeout time.Duration + output string + apiVersion string + bearerToken string ) func main() { @@ -109,12 +111,14 @@ func init() { rootCmd.PersistentFlags().DurationVar(&timeout, "timeout", 30*time.Second, "request timeout") rootCmd.PersistentFlags().StringVarP(&output, "output", "o", "table", "output format: table, json, yaml") rootCmd.PersistentFlags().StringVarP(&apiVersion, "version", "v", "", "API version to request (e.g., v1, v2beta1)") + rootCmd.PersistentFlags().StringVar(&bearerToken, "token", "", "JWT bearer token") // Bind flags to viper viper.BindPFlag("server", rootCmd.PersistentFlags().Lookup("server")) viper.BindPFlag("timeout", rootCmd.PersistentFlags().Lookup("timeout")) viper.BindPFlag("output", rootCmd.PersistentFlags().Lookup("output")) viper.BindPFlag("version", rootCmd.PersistentFlags().Lookup("version")) + viper.BindPFlag("token", rootCmd.PersistentFlags().Lookup("token")) // Environment variable support viper.SetEnvPrefix("FRU_TRACKER") @@ -159,6 +163,11 @@ func getClient() (*client.Client, error) { c = c.WithVersion(version) } + token := viper.GetString("token") + if token != "" { + c = c.WithBearerToken(token) + } + return c, nil } diff --git a/cmd/server/device_handlers_generated.go b/cmd/server/device_handlers_generated.go index 5a40baa..2403d62 100644 --- a/cmd/server/device_handlers_generated.go +++ b/cmd/server/device_handlers_generated.go @@ -1,8 +1,8 @@ // Code generated by Fabrica dev. DO NOT EDIT. // Template: server/handlers.go.tmpl -// Generated: 2026-03-26T14:33:28-07:00 +// Generated: 2026-05-05T20:59:07Z // -// # Copyright © 2025 OpenCHAMI a Series of LF Projects, LLC +// # Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // // SPDX-License-Identifier: MIT // @@ -10,7 +10,7 @@ // // To modify this code: // 1. Edit the template file: pkg/codegen/templates/handlers.go.tmpl -// 2. Run 'make dev' to regenerate +// 2. Run 'fabrica generate' to regenerate // 3. Do NOT edit this file directly - changes will be lost // // Generated handlers provide: diff --git a/cmd/server/discoverysnapshot_handlers_generated.go b/cmd/server/discoverysnapshot_handlers_generated.go index 4f63bd5..b555050 100644 --- a/cmd/server/discoverysnapshot_handlers_generated.go +++ b/cmd/server/discoverysnapshot_handlers_generated.go @@ -1,8 +1,8 @@ // Code generated by Fabrica dev. DO NOT EDIT. // Template: server/handlers.go.tmpl -// Generated: 2026-03-26T14:33:28-07:00 +// Generated: 2026-05-05T20:59:07Z // -// # Copyright © 2025 OpenCHAMI a Series of LF Projects, LLC +// # Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // // SPDX-License-Identifier: MIT // @@ -10,7 +10,7 @@ // // To modify this code: // 1. Edit the template file: pkg/codegen/templates/handlers.go.tmpl -// 2. Run 'make dev' to regenerate +// 2. Run 'fabrica generate' to regenerate // 3. Do NOT edit this file directly - changes will be lost // // Generated handlers provide: diff --git a/cmd/server/export.go b/cmd/server/export.go index 3360b23..bbee3be 100644 --- a/cmd/server/export.go +++ b/cmd/server/export.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026 OpenCHAMI Contributors +// +// SPDX-License-Identifier: MIT + package main import ( diff --git a/cmd/server/import.go b/cmd/server/import.go index b3744c1..afdad3c 100644 --- a/cmd/server/import.go +++ b/cmd/server/import.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026 OpenCHAMI Contributors +// +// SPDX-License-Identifier: MIT + package main import ( diff --git a/cmd/server/integration_test.go b/cmd/server/integration_test.go index 5829300..bf419c3 100644 --- a/cmd/server/integration_test.go +++ b/cmd/server/integration_test.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026 OpenCHAMI Contributors +// +// SPDX-License-Identifier: MIT + package main import ( @@ -21,7 +25,9 @@ import ( func TestDiscoverySnapshotIntegration(t *testing.T) { // 1. Setup in-memory Ent storage for the test client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") - defer client.Close() + t.Cleanup(func() { + require.NoError(t, client.Close()) + }) storage.SetEntClient(client) // 2. Register resource prefixes (required for CreateDiscoverySnapshot) @@ -35,7 +41,7 @@ func TestDiscoverySnapshotIntegration(t *testing.T) { // 4. Prepare the payload with at least one Node, one CPU, and one DIMM // Looking at apis/example.fabrica.dev/v1/device_types.go and reconcilers // DiscoverySnapshotSpec.RawData is expected to be a list of DeviceSpec - + nodeSerial := "node-001" cpuSerial := "cpu-001" dimmSerial := "dimm-001" diff --git a/cmd/server/main.go b/cmd/server/main.go index 2f28411..a2a1c11 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,4 +1,3 @@ - // Code generated by Fabrica dev. DO NOT EDIT. // Template: init/main.go.tmpl // Generated: 2026-02-26T11:09:26-08:00 @@ -19,38 +18,25 @@ import ( "syscall" "time" - "github.com/spf13/cobra" - "github.com/spf13/viper" + _ "github.com/example/fru-tracker/pkg/apiversion" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" - _ "github.com/example/fru-tracker/pkg/apiversion" "github.com/openchami/fabrica/pkg/versioning" + "github.com/spf13/cobra" + "github.com/spf13/viper" - - - - - "github.com/example/fru-tracker/internal/storage" - - + "github.com/example/fru-tracker/internal/storage" - "github.com/example/fru-tracker/internal/storage/ent" - "github.com/example/fru-tracker/internal/storage/ent/migrate" + "github.com/example/fru-tracker/internal/storage/ent" + "github.com/example/fru-tracker/internal/storage/ent/migrate" - _ "github.com/mattn/go-sqlite3" - - - - "github.com/openchami/fabrica/pkg/events" . "github.com/example/fru-tracker/internal/middleware" - + "github.com/openchami/fabrica/pkg/events" - - "github.com/openchami/fabrica/pkg/reconcile" "github.com/example/fru-tracker/pkg/reconcilers" - + "github.com/openchami/fabrica/pkg/reconcile" ) // Config holds all configuration for the service @@ -62,23 +48,16 @@ type Config struct { WriteTimeout int `mapstructure:"write_timeout"` IdleTimeout int `mapstructure:"idle_timeout"` - // Storage Configuration - - DatabaseURL string `mapstructure:"database-url"` - - - + DatabaseURL string `mapstructure:"database-url"` - // Reconciliation Configuration ReconcileEnabled bool `mapstructure:"reconcile_enabled"` ReconcileWorkers int `mapstructure:"reconcile_workers"` - // Feature Flags - + Debug bool `mapstructure:"debug"` } @@ -90,17 +69,12 @@ func DefaultConfig() *Config { ReadTimeout: 15, WriteTimeout: 15, IdleTimeout: 60, - - - DatabaseURL: "file:./data.db?cache=shared&_fk=1", - - - - + + DatabaseURL: "file:./data.db?cache=shared&_fk=1", + ReconcileEnabled: true, ReconcileWorkers: 5, - - + Debug: false, } } @@ -144,15 +118,7 @@ func init() { serveCmd.Flags().Int("write-timeout", 15, "Write timeout in seconds") serveCmd.Flags().Int("idle-timeout", 60, "Idle timeout in seconds") - - serveCmd.Flags().String("database-url", "", "Database connection URL") - - - - - - // Bind flags to viper viper.BindPFlags(serveCmd.Flags()) @@ -160,14 +126,14 @@ func init() { // Add subcommands rootCmd.AddCommand(serveCmd) - + rootCmd.AddCommand(versionCmd) - + // Generated by Fabrica v0.4.0+: export and import commands (Ent storage only) - + rootCmd.AddCommand(newExportCommand()) rootCmd.AddCommand(newImportCommand()) - + } func initConfig() { @@ -210,9 +176,8 @@ func initConfig() { func runServer(cmd *cobra.Command, args []string) error { log.Printf("Starting fru-tracker server...") - // Initialize storage backend - + // Connect to database client, err := ent.Open("sqlite3", config.DatabaseURL) if err != nil { @@ -234,10 +199,7 @@ func runServer(cmd *cobra.Command, args []string) error { // Set Ent client for storage operations storage.SetEntClient(client) log.Printf("Ent storage initialized with sqlite3 database") - - - // Initialize event system with configuration from environment eventConfig := &events.EventConfig{ Enabled: true, @@ -263,29 +225,27 @@ func runServer(cmd *cobra.Command, args []string) error { // Initialize event bridge for condition events events.InitializeEventBridge() - // Initialize ONE event bus for handlers AND reconcilers - log.Println("Initializing single event bus...") - - eventBus := events.NewInMemoryEventBus(1000, 10) - - eventBus.Start() - defer eventBus.Close() // Defer close here, at the top level + // Initialize ONE event bus for handlers AND reconcilers + log.Println("Initializing single event bus...") + + eventBus := events.NewInMemoryEventBus(1000, 10) - // Set the global instance for handlers - // This replaces the call to InitializeEventBus() - events.SetGlobalEventBus(eventBus) - GlobalEventBus = eventBus // Set the global var from event_bus_generated.go - log.Println("Global event bus started and set.") + eventBus.Start() + defer eventBus.Close() // Defer close here, at the top level + + // Set the global instance for handlers + // This replaces the call to InitializeEventBus() + events.SetGlobalEventBus(eventBus) + GlobalEventBus = eventBus // Set the global var from event_bus_generated.go + log.Println("Global event bus started and set.") log.Printf("Event system initialized - Lifecycle: %v, Conditions: %v, Prefix: %s", eventConfig.LifecycleEventsEnabled, eventConfig.ConditionEventsEnabled, eventConfig.EventTypePrefix) - - // Initialize reconciliation controller // Note: This requires reconciliation code to be generated via 'fabrica generate' // and reconcilers to be implemented in pkg/reconcilers/ - + if config.ReconcileEnabled { ctx := context.Background() @@ -309,8 +269,6 @@ func runServer(cmd *cobra.Command, args []string) error { log.Printf("Reconciliation controller started with %d workers", 5) } - - // Register resource prefixes for UID generation // This is required before any handlers can create resources @@ -337,14 +295,10 @@ func runServer(cmd *cobra.Command, args []string) error { r.Mount("/debug", middleware.Profiler()) } - - // Register routes - generated by 'fabrica generate' RegisterGeneratedRoutes(r) r.Get("/health", healthHandler) - - // Create HTTP server addr := fmt.Sprintf("%s:%d", config.Host, config.Port) server := &http.Server{ @@ -358,12 +312,8 @@ func runServer(cmd *cobra.Command, args []string) error { // Start server in goroutine go func() { log.Printf("Server starting on %s", addr) - - + log.Printf("Storage: sqlite3 database") - - - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("Server failed: %v", err) @@ -397,9 +347,6 @@ func healthHandler(w http.ResponseWriter, r *http.Request) { // registerResourcePrefixes is defined in routes_generated.go - - - var versionCmd = &cobra.Command{ Use: "version", Short: "Print the version number", @@ -408,4 +355,3 @@ var versionCmd = &cobra.Command{ fmt.Println("fru-tracker v1.0.0") }, } - diff --git a/cmd/server/models_generated.go b/cmd/server/models_generated.go index eb8e7a8..baef7e8 100644 --- a/cmd/server/models_generated.go +++ b/cmd/server/models_generated.go @@ -1,5 +1,5 @@ // Code generated by Fabrica dev. DO NOT EDIT. -// Copyright © 2025 OpenCHAMI a Series of LF Projects, LLC +// Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // // SPDX-License-Identifier: MIT // @@ -8,7 +8,7 @@ // // To modify request/response models: // 1. Edit the template file: pkg/codegen/templates/models.go.tmpl -// 2. Run 'make dev' to regenerate +// 2. Run 'fabrica generate' to regenerate // 3. Do NOT edit this file directly - changes will be lost // // Generated models for each resource: diff --git a/cmd/server/openapi_extensions.go b/cmd/server/openapi_extensions.go new file mode 100644 index 0000000..3c3dbaa --- /dev/null +++ b/cmd/server/openapi_extensions.go @@ -0,0 +1,47 @@ +// Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC +// +// SPDX-License-Identifier: MIT +// +// This file contains the user-editable OpenAPI extension hook. +// +// ✅ This file is safe to edit: it will NOT be overwritten by regeneration. +// +// Add any routes that are not Fabrica-generated (legacy APIs, custom endpoints, +// WireGuard, cloud-init, etc.) to registerCustomOpenAPIPaths so they appear in +// the served OpenAPI spec and Swagger UI at /openapi.json and /docs. +// +// Example: +// +// func registerCustomOpenAPIPaths(spec *openapi3.T) { +// metaDataOp := openapi3.NewOperation() +// metaDataOp.OperationID = "getMetaData" +// metaDataOp.Summary = "Cloud-init meta-data endpoint" +// metaDataOp.Tags = []string{"cloud-init"} +// metaDataOp.Responses = openapi3.NewResponses() +// metaDataOp.Responses.Set("200", &openapi3.ResponseRef{ +// Value: openapi3.NewResponse().WithDescription("YAML metadata for the requesting node"), +// }) +// spec.Paths.Set("/meta-data", &openapi3.PathItem{Get: metaDataOp}) +// } +package main + +import "github.com/getkin/kin-openapi/openapi3" + +// registerCustomOpenAPIPaths is called by GenerateOpenAPISpec after all +// Fabrica-generated resource paths have been registered. +// Add your custom / non-generated route definitions here. +func registerCustomOpenAPIPaths(spec *openapi3.T) { + // Add custom route definitions here. + // Example (uncomment and extend as needed): + // + // op := openapi3.NewOperation() + // op.OperationID = "myCustomEndpoint" + // op.Summary = "My custom endpoint" + // op.Tags = []string{"Custom"} + // op.Responses = openapi3.NewResponses() + // op.Responses.Set("200", &openapi3.ResponseRef{ + // Value: openapi3.NewResponse().WithDescription("OK"), + // }) + // spec.Paths.Set("/my-endpoint", &openapi3.PathItem{Get: op}) + _ = spec +} diff --git a/cmd/server/openapi_generated.go b/cmd/server/openapi_generated.go index 544f9fa..233a7a6 100644 --- a/cmd/server/openapi_generated.go +++ b/cmd/server/openapi_generated.go @@ -1,5 +1,5 @@ -// Code generated by codegen. DO NOT EDIT. -// Copyright © 2025 OpenCHAMI a Series of LF Projects, LLC +// Code generated by Fabrica. DO NOT EDIT. +// Copyright © 2026 OpenCHAMI a Series of LF Projects, LLC // // SPDX-License-Identifier: MIT // @@ -8,7 +8,7 @@ // // To modify OpenAPI spec: // 1. Edit the template file: pkg/codegen/templates/openapi.go.tmpl -// 2. Run 'make dev' to regenerate +// 2. Run 'fabrica generate' to regenerate // 3. Do NOT edit this file directly - changes will be lost // // OpenAPI endpoints: @@ -43,7 +43,7 @@ func ServeSwaggerUI(w http.ResponseWriter, r *http.Request) { - OpenCHAMI Inventory API Documentation + fru_tracker API Documentation