Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
Virtualization:containers
frizbee
frizbee-0.1.2.obscpio
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File frizbee-0.1.2.obscpio of Package frizbee
07070100000000000081A400000000000000000000000166CD8D32000000EF000000000000000000000000000000000000001B00000000frizbee-0.1.2/.frizbee.ymlghactions: exclude: # Exclude the SLSA GitHub Generator workflow. # See https://github.com/slsa-framework/slsa-github-generator/issues/2993 - slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml 07070100000001000041ED00000000000000000000000266CD8D3200000000000000000000000000000000000000000000001600000000frizbee-0.1.2/.github07070100000002000041ED00000000000000000000000266CD8D3200000000000000000000000000000000000000000000002500000000frizbee-0.1.2/.github/ISSUE_TEMPLATE07070100000003000081A400000000000000000000000166CD8D3200000401000000000000000000000000000000000000003400000000frizbee-0.1.2/.github/ISSUE_TEMPLATE/bug_report.ymlname: Report a bug description: Tell us about a bug or issue you may have identified. title: "Provide a general summary of the issue" labels: [bug] assignees: "-" body: - type: markdown attributes: value: "To expedite our maintainers' efforts, please search for [duplicate or closed issues](https://github.com/stacklok/frizbee/issues?q=is%3Aissue+) and familiarize yourself with our [contributing guidelines](https://github.com/stacklok/frizbee/blob/main/CONTRIBUTING.md)." - type: textarea id: what-happened attributes: label: Describe the issue description: A clear and concise description of what the bug is. If applicable, add screenshots to illustrate the problem. validations: required: true - type: textarea id: reproduce-steps attributes: label: To Reproduce description: Describe the steps to reproduce the behavior. - type: input id: version attributes: label: What version are you using? placeholder: "e.g., v5.1.0 or v4.5.2" 07070100000004000081A400000000000000000000000166CD8D32000000B1000000000000000000000000000000000000003000000000frizbee-0.1.2/.github/ISSUE_TEMPLATE/config.ymlcontact_links: - name: Ask the community url: https://discord.com/invite/RkzVuTp3WK about: Ask and discuss questions with other Frizbee community members on Discord. 07070100000005000081A400000000000000000000000166CD8D32000004EF000000000000000000000000000000000000003500000000frizbee-0.1.2/.github/ISSUE_TEMPLATE/enhancement.ymlname: Feature request description: Suggest new or updated features to include in Frizbee. title: "Suggest a new feature" labels: [user-story] assignees: "-" body: - type: textarea id: description attributes: label: Please describe the enhancement description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...], this could be improved by [...], this would provide the ability to [...] validations: required: true - type: textarea id: solution attributes: label: Solution Proposal description: A clear and concise description of what you want to happen. validations: required: true - type: textarea id: considerations attributes: label: Describe alternatives you've considered description: A clear and concise description of any alternative solutions or features you've considered. - type: textarea id: context attributes: label: Additional context description: Add any other context about the feature request here. e.g. link to a Google doc if the design is being discussed there. - type: textarea id: acceptance attributes: label: Acceptance Criteria description: What does done look like? 07070100000006000081A400000000000000000000000166CD8D32000000EE000000000000000000000000000000000000002500000000frizbee-0.1.2/.github/dependabot.ymlversion: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" open-pull-requests-limit: 10 - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" 07070100000007000041ED00000000000000000000000266CD8D3200000000000000000000000000000000000000000000002000000000frizbee-0.1.2/.github/workflows07070100000008000081A400000000000000000000000166CD8D3200000E88000000000000000000000000000000000000002B00000000frizbee-0.1.2/.github/workflows/codeql.yml# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ "main" ] pull_request: branches: [ "main" ] schedule: - cron: '36 6 * * 0' jobs: analyze: name: Analyze # Runner size impacts CodeQL analysis time. To learn more, please see: # - https://gh.io/recommended-hardware-resources-for-running-codeql # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners # Consider using larger runners for possible analysis time improvements. runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'go' ] # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - name: Checkout repository uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v3 - name: Setup Go uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v4 with: go-version-file: ./go.mod # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@6fcbee77eeb7314eb10f949471de9856865a5f9b # v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@6fcbee77eeb7314eb10f949471de9856865a5f9b # v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # If the Autobuild fails above, remove it and uncomment the following three lines. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # - run: | # echo "Run, Build Application using script" # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@6fcbee77eeb7314eb10f949471de9856865a5f9b # v2 with: category: "/language:${{matrix.language}}" 07070100000009000081A400000000000000000000000166CD8D3200001890000000000000000000000000000000000000002D00000000frizbee-0.1.2/.github/workflows/releaser.yml# # Copyright 2023 Stacklok, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # This workflow compiles frizbee using a SLSA3 compliant # build and then verifies the provenance of the built artifacts. # It releases the following architectures: amd64, arm64, and armv7 on Linux, # Windows, and macOS. # The provenance file can be verified using https://github.com/slsa-framework/slsa-verifier. # For more information about SLSA and how it improves the supply-chain, visit slsa.dev. name: Release on: push: tags: - '*' permissions: contents: write jobs: ldflags_args: runs-on: ubuntu-latest outputs: commit-date: ${{ steps.ldflags.outputs.commit-date }} commit: ${{ steps.ldflags.outputs.commit }} version: ${{ steps.ldflags.outputs.version }} tree-state: ${{ steps.ldflags.outputs.tree-state }} steps: - id: checkout uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 with: fetch-depth: 0 - id: ldflags run: | echo "commit=$GITHUB_SHA" >> $GITHUB_OUTPUT echo "commit-date=$(git log --date=iso8601-strict -1 --pretty=%ct)" >> $GITHUB_OUTPUT echo "version=$(git describe --tags --always --dirty | cut -c2-)" >> $GITHUB_OUTPUT echo "tree-state=$(if git diff --quiet; then echo "clean"; else echo "dirty"; fi)" >> $GITHUB_OUTPUT release: needs: - ldflags_args name: Build and release outputs: hashes: ${{ steps.hash.outputs.hashes }} permissions: contents: write # To add assets to a release. id-token: write # To do keyless signing with cosign runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 with: fetch-depth: 0 - name: Setup Go uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v4 with: go-version-file: 'go.mod' cache: true - name: Install Syft uses: anchore/sbom-action/download-syft@61119d458adab75f756bc0b9e4bde25725f86a7a # v0.17.2 - name: Install Cosign uses: sigstore/cosign-installer@4959ce089c160fddf62f7b42464195ba1a56d382 # v3.6.0 - name: Run GoReleaser id: run-goreleaser uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v5 with: distribution: goreleaser version: "~> v2" args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} WINGET_GITHUB_TOKEN: ${{ secrets.WINGET_GITHUB_TOKEN }} VERSION: ${{ needs.ldflags_args.outputs.version }} COMMIT: ${{ needs.ldflags_args.outputs.commit }} COMMIT_DATE: ${{ needs.ldflags_args.outputs.commit-date }} TREE_STATE: ${{ needs.ldflags_args.outputs.tree-state }} - name: Generate subject id: hash env: ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}" run: | set -euo pipefail hashes=$(echo $ARTIFACTS | jq --raw-output '.[] | {name, "digest": (.extra.Digest // .extra.Checksum)} | select(.digest) | {digest} + {name} | join(" ") | sub("^sha256:";"")' | base64 -w0) if test "$hashes" = ""; then # goreleaser < v1.13.0 checksum_file=$(echo "$ARTIFACTS" | jq -r '.[] | select (.type=="Checksum") | .path') hashes=$(cat $checksum_file | base64 -w0) fi echo "hashes=$hashes" >> $GITHUB_OUTPUT provenance: name: Generate provenance (SLSA3) needs: - release permissions: actions: read # To read the workflow path. id-token: write # To sign the provenance. contents: write # To add assets to a release. uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 with: base64-subjects: "${{ needs.release.outputs.hashes }}" upload-assets: true # upload to a new release verification: name: Verify provenance of assets (SLSA3) needs: - release - provenance runs-on: ubuntu-latest permissions: read-all steps: - name: Install the SLSA verifier uses: slsa-framework/slsa-verifier/actions/installer@3714a2a4684014deb874a0e737dffa0ee02dd647 # v2.6.0 - name: Download assets env: GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}" CHECKSUMS: "${{ needs.release.outputs.hashes }}" ATT_FILE_NAME: "${{ needs.provenance.outputs.provenance-name }}" run: | set -euo pipefail checksums=$(echo "$CHECKSUMS" | base64 -d) while read -r line; do fn=$(echo $line | cut -d ' ' -f2) echo "Downloading $fn" gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "$fn" done <<<"$checksums" gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "$ATT_FILE_NAME" - name: Verify assets env: CHECKSUMS: "${{ needs.release.outputs.hashes }}" PROVENANCE: "${{ needs.provenance.outputs.provenance-name }}" run: | set -euo pipefail checksums=$(echo "$CHECKSUMS" | base64 -d) while read -r line; do fn=$(echo $line | cut -d ' ' -f2) echo "Verifying SLSA provenance for $fn" slsa-verifier verify-artifact --provenance-path "$PROVENANCE" \ --source-uri "github.com/$GITHUB_REPOSITORY" \ --source-tag "$GITHUB_REF_NAME" \ "$fn" done <<<"$checksums" 0707010000000A000081A400000000000000000000000166CD8D3200000D89000000000000000000000000000000000000002900000000frizbee-0.1.2/.github/workflows/test.ymlname: CI on: push: branches: - main pull_request: branches: - main jobs: build: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: setup go uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v4 with: go-version-file: ./go.mod - name: setup task uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611 # v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} # Builds the binary into the bin/ directory - name: build run: |- task build - name: upload artifact uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: frizbee path: bin/frizbee cover: name: Coverage permissions: contents: read runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Set up Go uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v4 with: go-version-file: 'go.mod' - name: setup task uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611 # v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Run coverage run: task cover # Using gcov didn't seem to work for the coveralls app, so we convert it to lcov - name: Try converting to LCOV run: go run github.com/jandelgado/gcov2lcov@latest -infile=./coverage.out -outfile=./coverage.lcov - name: Coveralls GitHub Action uses: coverallsapp/github-action@643bc377ffa44ace6394b2b5d0d3950076de9f63 # v2.3.0 lint: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: setup go uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v4 with: go-version-file: ./go.mod - name: golangci-lint uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v3 test: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: setup go uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v4 with: go-version-file: ./go.mod - name: setup task uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611 # v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - name: test run: |- task test env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Checks that the github workflows are valid using this same tool frizbee: runs-on: ubuntu-latest needs: - build steps: - name: checkout uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: download artifact uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 #v4.1.8 with: name: frizbee path: bin/ - name: Make frizbee executable run: |- chmod +x bin/frizbee - name: Frizbee run: |- bin/frizbee actions --dry-run --error env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 0707010000000B000081A400000000000000000000000166CD8D32000001D7000000000000000000000000000000000000002A00000000frizbee-0.1.2/.github/workflows/trivy.ymlname: Trivy Scan on: pull_request: jobs: trivy-code-security-scan: runs-on: ubuntu-latest name: Trivy steps: - name: Checkout uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Security Scan uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # 0.24.0 with: scan-type: 'fs' scanners: vuln,secret exit-code: 1 ignore-unfixed: true0707010000000C000081A400000000000000000000000166CD8D32000001F9000000000000000000000000000000000000001900000000frizbee-0.1.2/.gitignore# If you prefer the allow list template instead of the deny list, see community template: # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib bin/ dist/ # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ # Go workspace file go.work .idea/ frizbee0707010000000D000081A400000000000000000000000166CD8D32000009D2000000000000000000000000000000000000001C00000000frizbee-0.1.2/.golangci.ymlrun: go: "1.21" issues-exit-code: 1 timeout: 5m linters-settings: lll: line-length: 130 gocyclo: min-complexity: 15 gci: sections: - standard - default - prefix(github.com/stacklok/frizbee) revive: # see https://github.com/mgechev/revive#available-rules for details. ignore-generated-header: true severity: warning rules: - name: blank-imports severity: warning - name: context-as-argument - name: context-keys-type - name: duplicated-imports - name: error-naming # - name: error-strings #BDG: This was enabled for months, but it suddenly started working on 3/2/2022.. come to find out we have TONS of error messages starting with capital... disabling for now(ever?) - name: error-return - name: exported severity: error - name: if-return # - name: get-return // BDG: We have a lot of API endpoint handlers named like getFoos but write to response vs return... maybe later can figure that out - name: identical-branches - name: indent-error-flow - name: import-shadowing - name: package-comments - name: range-val-in-closure - name: range-val-address - name: redefines-builtin-id - name: struct-tag - name: unconditional-recursion - name: unnecessary-stmt - name: unreachable-code - name: unused-parameter - name: unused-receiver - name: unhandled-error disabled: true linters: disable-all: true enable: - asasalint - asciicheck - bidichk - bodyclose - contextcheck - decorder - dogsled - dupl - errcheck - errname - exhaustive - exportloopref - forbidigo - forcetypeassert - gci - gochecknoglobals - gochecknoinits - gochecksumtype - goconst - gocyclo - gofmt - goprintffuncname - gosec - gosimple - gosimple - govet - importas - inamedparam - ineffassign - interfacebloat - lll - makezero - mirror - noctx - nosprintfhostport - paralleltest - perfsprint - promlinter - revive - staticcheck - tenv - thelper - tparallel - unparam - unused - usestdlibvars issues: exclude-use-default: false exclude-rules: - path: '(.+)_test\.go' linters: - lll output: formats: - format: colored-line-number print-issued-lines: true print-linter-name: true sort-results: true 0707010000000E000081A400000000000000000000000166CD8D3200000D12000000000000000000000000000000000000001F00000000frizbee-0.1.2/.goreleaser.yaml# yaml-language-server: $schema=https://goreleaser.com/static/schema.json # vim: set ts=2 sw=2 tw=0 fo=cnqoj project_name: frizbee version: 2 # This section defines the build matrix. builds: - env: - GO111MODULE=on - CGO_ENABLED=0 flags: - -trimpath - -tags=netgo ldflags: - "-s -w" - "-X main.Version={{ .Env.VERSION }}" - "-X main.Commit={{ .Env.COMMIT }}" - "-X main.CommitDate={{ .Env.COMMIT_DATE }}" - "-X main.TreeState={{ .Env.TREE_STATE }}" - "-X github.com/stacklok/frizbee/internal/cli.CLIVersion={{ .Env.VERSION }}" goos: - linux - windows - darwin goarch: - amd64 - arm64 main: ./main.go # This section defines the release format. archives: - format: tar.gz # we can use binary, but it seems there's an issue where goreleaser skips the sboms name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" format_overrides: - goos: windows format: zip # This section defines how to release to homebrew. brews: - homepage: 'https://github.com/stacklok/frizbee' description: 'frizbee is a tool you may throw a tag at and it comes back with a checksum.' directory: Formula commit_author: name: stacklokbot email: info@stacklok.com repository: owner: stacklok name: homebrew-tap token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" test: | system "#{bin}/frizbee --help" # This section defines how to release to winget. winget: - name: frizbee publisher: stacklok license: Apache-2.0 license_url: "https://github.com/stacklok/frizbee/blob/main/LICENSE" copyright: Stacklok, Inc. homepage: https://stacklok.com short_description: 'frizbee is a tool you may throw a tag at and it comes back with a checksum.' publisher_support_url: "https://github.com/stacklok/frizbee/issues/new/choose" package_identifier: "stacklok.frizbee" url_template: "https://github.com/stacklok/frizbee/releases/download/{{ .Tag }}/{{ .ArtifactName }}" skip_upload: auto release_notes: "{{.Changelog}}" tags: - golang - cli commit_author: name: stacklokbot email: info@stacklok.com goamd64: v1 repository: owner: stacklok name: winget-pkgs branch: "frizbee-{{.Version}}" token: "{{ .Env.WINGET_GITHUB_TOKEN }}" pull_request: enabled: true draft: false base: owner: microsoft name: winget-pkgs branch: master # This section defines whether we want to release the source code too. source: enabled: true # This section defines how to generate the changelog changelog: sort: asc use: github # This section defines for which artifact types to generate SBOMs. sboms: - artifacts: archive # This section defines the release policy. release: github: owner: stacklok name: frizbee # This section defines how and which artifacts we want to sign for the release. signs: - cmd: cosign args: - "sign-blob" - "--output-signature=${signature}" - "--output-certificate=${certificate}" - "${artifact}" - "--yes" # needed on cosign 2.0.0+ artifacts: archive output: true certificate: '{{ trimsuffix (trimsuffix .Env.artifact ".zip") ".tar.gz" }}.pem' 0707010000000F000081A400000000000000000000000166CD8D3200000C93000000000000000000000000000000000000002100000000frizbee-0.1.2/CODE_OF_CONDUCT.md# Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at <code-of-conduct@stacklok.com>. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ 07070100000010000081A400000000000000000000000166CD8D3200000EA0000000000000000000000000000000000000001E00000000frizbee-0.1.2/CONTRIBUTING.md # Contributing to Frizbee First off, thank you for taking the time to contribute to Frizbee! :+1: :tada: Frizbee is released under the Apache 2.0 license. If you would like to contribute something or want to hack on the code, this document should help you get started. You can find some hints for starting development in Frizbee's [README](https://github.com/stacklok/frizbee/blob/main/README.md). ## Table of contents - [Code of Conduct](#code-of-conduct) - [Reporting Security Vulnerabilities](#reporting-security-vulnerabilities) - [How to Contribute](#how-to-contribute) - [Sign the Contributor License Agreement](#sign-the-contributor-license-agreement) - [Using GitHub Issues](#using-github-issues) - [Not sure how to start contributing...](#not-sure-how-to-start-contributing) - [Pull Request Process](#pull-request-process) - [Contributing to docs](#contributing-to-docs) - [Commit Message Guidelines](#commit-message-guidelines) ## Code of Conduct This project adheres to the [Contributor Covenant](https://github.com/stacklok/frizbee/blob/main/CODE_OF_CONDUCT.md) code of conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior to code-of-conduct@stacklok.dev. ## Reporting Security Vulnerabilities If you think you have found a security vulnerability in Frizbee please DO NOT disclose it publicly until we’ve had a chance to fix it. Please don’t report security vulnerabilities using GitHub issues; instead, please follow this [process](https://github.com/stacklok/frizbee/blob/main/SECURITY.md) ## How to Contribute ### Using GitHub Issues We use GitHub issues to track bugs and enhancements. If you have a general usage question, please ask in [Frizbee's discussion forum](https://discord.com/invite/RkzVuTp3WK). If you are reporting a bug, please help to speed up problem diagnosis by providing as much information as possible. Ideally, that would include a small sample project that reproduces the problem. ### Sign the Contributor License Agreement Before we accept a non-trivial patch or pull request, we will need you to sign the [Contributor License Agreement](https://cla-assistant.io/stacklok/frizbee). Signing the contributor’s agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. Active contributors might be asked to join the core team and given the ability to merge pull requests. ### Not sure how to start contributing... PRs to resolve existing issues are greatly appreciated and issues labeled as ["good first issue"](https://github.com/stacklok/frizbee/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) are a great place to start! ### Pull Request Process * Create an issue outlining the fix or feature. * Fork the Frizbee repository to your own GitHub account and clone it locally. * Hack on your changes. * Correctly format your commit messages, see [Commit Message Guidelines](#Commit-Message-Guidelines) below. * Open a PR by ensuring the title and its description reflect the content of the PR. * Ensure that CI passes, if it fails, fix the failures. * Every pull request requires a review from the core Frizbee team before merging. * Once approved, all of your commits will be squashed into a single commit with your PR title. ### Contributing to docs Follow [this guide](https://github.com/stacklok/frizbee/blob/main/docs/README.md) for instructions on building, running, and previewing Miner's documentation. ### Commit Message Guidelines We follow the commit formatting recommendations found on [Chris Beams' How to Write a Git Commit Message article](https://chris.beams.io/posts/git-commit/). 07070100000011000081A400000000000000000000000166CD8D3200002C50000000000000000000000000000000000000001600000000frizbee-0.1.2/LICENSE Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2023 Stacklok, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 07070100000012000081A400000000000000000000000166CD8D32000019D2000000000000000000000000000000000000001800000000frizbee-0.1.2/README.md![image](https://github.com/stacklok/frizbee/assets/16540482/35034046-d962-475d-b8e2-67b7625f2a60) --- [![Coverage Status](https://coveralls.io/repos/github/stacklok/frizbee/badge.svg?branch=main)](https://coveralls.io/github/stacklok/frizbee?branch=main) | [![License: Apache 2.0](https://img.shields.io/badge/License-Apache2.0-brightgreen.svg)](https://opensource.org/licenses/Apache-2.0) | [![](https://dcbadge.vercel.app/api/server/RkzVuTp3WK?logo=discord&label=Discord&color=5865&style=flat)](https://discord.gg/RkzVuTp3WK) --- # Frizbee Frizbee is a tool you may throw a tag at and it comes back with a checksum. It's a command-line tool designed to provide checksums for GitHub Actions and container images based on tags. It also includes a set of libraries for working with tags and checksums. Frizbee is available as a GitHub Action: [frizbee-action](https://github.com/marketplace/actions/frizbee-action) ## Table of Contents - [Installation](#installation) - [Usage - CLI](#usage---cli) - [GitHub Actions](#github-actions) - [Container Images](#container-images) - [Usage - Library](#usage---library) - [GitHub Actions](#github-actions) - [Container Images](#container-images) - [Configuration](#configuration) - [Contributing](#contributing) - [License](#license) ## Installation To install Frizbee, you can use the following methods: ```bash # Using Go go get -u github.com/stacklok/frizbee go install github.com/stacklok/frizbee # Using Homebrew brew install frizbee # Using winget winget install stacklok.frizbee ``` ## Usage - CLI ### GitHub Actions Frizbee can be used to generate checksums for GitHub Actions. This is useful for verifying that the contents of a GitHub Action have not changed. To quickly replace the GitHub Action references for your project, you can use the `actions` command: ```bash frizbee actions path/to/your/repo/.github/workflows/ ``` This will write all the replacements to the files in the directory provided. Note that this command will only replace the `uses` field of the GitHub Action references. Note that this command supports dry-run mode, which will print the replacements to stdout instead of writing them to the files. It also supports exiting with a non-zero exit code if any replacements are found. This is handy for CI/CD pipelines. If you want to generate the replacement for a single GitHub Action, you can use the same command: ```bash frizbee actions metal-toolbox/container-push/.github/workflows/container-push.yml@main ``` This is useful if you're developing and want to quickly test the replacement. ### Container Images Frizbee can be used to generate checksums for container images. This is useful for verifying that the contents of a container image have not changed. This works for all yaml/yml and Dockerfile fies in the directory provided by the `-d` flag. To quickly replace the container image references for your project, you can use the `image` command: ```bash frizbee image path/to/your/yaml/files/ ``` To get the digest for a single image tag, you can use the same command: ```bash frizbee image ghcr.io/stacklok/minder/server:latest ``` This will print the image reference with the digest for the image tag provided. ## Usage - Library Frizbee can also be used as a library. The library provides a set of functions for working with tags and checksums. Here are a few examples of how you can use the library: ### GitHub Actions ```go // Create a new replacer r := replacer.NewGitHubActionsReplacer(config.DefaultConfig()) ... // Parse a single GitHub Action reference ret, err := r.ParseString(ctx, ghActionRef) ... // Parse all GitHub Actions workflow yaml files in a given directory res, err := r.ParsePath(ctx, dir) ... // Parse and replace all GitHub Actions references in the provided file system res, err := r.ParsePathInFS(ctx, bfs, base) ... // Parse a single yaml file referencing GitHub Actions res, err := r.ParseFile(ctx, fileHandler) ... // List all GitHub Actions referenced in the given directory res, err := r.ListPath(dir) ... // List all GitHub Actions referenced in the provided file system res, err := r.ListPathInFS(bfs, base) ... // List all GitHub Actions referenced in the provided file res, err := r.ListFile(fileHandler) ``` ### Container images ```go // Create a new replacer r := replacer.NewContainerImagesReplacer(config.DefaultConfig()) ... // Parse a single container image reference ret, err := r.ParseString(ctx, ghActionRef) ... // Parse all files containing container image references in a given directory res, err := r.ParsePath(ctx, dir) ... // Parse and replace all container image references in the provided file system res, err := r.ParsePathInFS(ctx, bfs, base) ... // Parse a single yaml file referencing container images res, err := r.ParseFile(ctx, fileHandler) ... // List all container images referenced in the given directory res, err := r.ListPath(dir) ... // List all container images referenced in the provided file system res, err := r.ListPathInFS(bfs, base) ... // List all container images referenced in the provided file res, err := r.ListFile(fileHandler) ``` ## Configuration Frizbee can be configured by setting up a `.frizbee.yml` file. You can configure Frizbee to skip processing certain actions, i.e. ```yml ghactions: exclude: # Exclude the SLSA GitHub Generator workflow. # See https://github.com/slsa-framework/slsa-github-generator/issues/2993 - slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml ``` Similarly, you can exclude actions that are referenced using a particular branch: ```yml ghactions: exclude_branches: - main - master ``` By default, Frizbee will exclude all actions that are referencing `main` or `master`. You can also configure Frizbee to skip processing certain container images or certain tags: ```yml images: exclude_images: - busybox exclude_tags: - devel ``` By default, Frizbee will exclude the image named `scratch` and the tag `latest`. ## Contributing & Community Frizbee is maintained by a dedicated community of developers that want this open souce project to benefit others and thrive. The main development of Frizbee is done in [Go](https://go.dev/). We welcome contributions of all types! Please see our [Contributing](./CONTRIBUTING.md) guide for more information on how you can help! If you have questions, or just want to chat with us - please use the #frizbee channel on our [Discord Server](https://discord.gg/stacklok). ## License Frizbee is licensed under the [Apache 2.0 License](./LICENSE). 07070100000013000081A400000000000000000000000166CD8D3200000333000000000000000000000000000000000000001A00000000frizbee-0.1.2/SECURITY.md# Security Policy ## Reporting a Vulnerability The Frizbee team and community take security seriously! We appreciate your efforts to disclose your findings responsibly and will make every effort to acknowledge your contributions. To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/stacklok/frizbee/security/advisories/new) tab. The Frizbee team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will inform you of the progress toward a fix and public disclosure. Throughout this process, we may ask for additional information or guidance to ensure we have fixed the issue. Please report security bugs in third-party modules to the person or team maintaining the module. 07070100000014000081A400000000000000000000000166CD8D32000001F7000000000000000000000000000000000000001B00000000frizbee-0.1.2/Taskfile.yml--- version: '3' tasks: default: silent: true cmds: - task -l test: desc: Run tests cmds: - go test -v ./... cover: desc: Run coverage cmds: - go test -coverprofile=coverage.out ./... build: desc: Build the frizbee binary cmds: - go build -ldflags="-X github.com/stacklok/frizbee/pkg/constants.CLIVersion=dev" -o ./bin/ ./... lint: desc: Run linter cmds: - golangci-lint run --timeout 5m0s --config .golangci.yml07070100000015000041ED00000000000000000000000266CD8D3200000000000000000000000000000000000000000000001200000000frizbee-0.1.2/cmd07070100000016000041ED00000000000000000000000266CD8D3200000000000000000000000000000000000000000000001A00000000frizbee-0.1.2/cmd/actions07070100000017000081A400000000000000000000000166CD8D3200000BD9000000000000000000000000000000000000002500000000frizbee-0.1.2/cmd/actions/actions.go// // Copyright 2024 Stacklok, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package actions provides command-line utilities to work with GitHub Actions. package actions import ( "errors" "fmt" "os" "path/filepath" "github.com/spf13/cobra" "github.com/stacklok/frizbee/internal/cli" "github.com/stacklok/frizbee/pkg/interfaces" "github.com/stacklok/frizbee/pkg/replacer" "github.com/stacklok/frizbee/pkg/utils/config" ) // CmdGHActions represents the actions command func CmdGHActions() *cobra.Command { cmd := &cobra.Command{ Use: "actions", Short: "Replace tags in GitHub Actions workflows", Long: `This utility replaces tag or branch references in GitHub Actions workflows with the latest commit hash of the referenced tag or branch. Example: $ frizbee actions <.github/workflows> or <actions/checkout@v4> This will replace all tag or branch references in all GitHub Actions workflows for the given directory. Supports both directories and single references. ` + cli.TokenHelpText + "\n", Aliases: []string{"ghactions"}, // backwards compatibility RunE: replaceCmd, SilenceUsage: true, Args: cobra.MaximumNArgs(1), } // flags cli.DeclareFrizbeeFlags(cmd, false) // sub-commands cmd.AddCommand(CmdList()) return cmd } // nolint:errcheck func replaceCmd(cmd *cobra.Command, args []string) error { // Set the default directory if not provided pathOrRef := ".github/workflows" if len(args) > 0 { pathOrRef = args[0] } // Extract the CLI flags from the cobra command cliFlags, err := cli.NewHelper(cmd) if err != nil { return err } // Set up the config cfg, err := config.FromCommand(cmd) if err != nil { return err } // Create a new replacer r := replacer.NewGitHubActionsReplacer(cfg). WithUserRegex(cliFlags.Regex). WithGitHubClientFromToken(os.Getenv(cli.GitHubTokenEnvKey)) if cli.IsPath(pathOrRef) { dir := filepath.Clean(pathOrRef) // Replace the tags in the given directory res, err := r.ParsePath(cmd.Context(), dir) if err != nil { return err } // Process the output files return cliFlags.ProcessOutput(dir, res.Processed, res.Modified) } // Replace the passed reference res, err := r.ParseString(cmd.Context(), pathOrRef) if err != nil { if errors.Is(err, interfaces.ErrReferenceSkipped) { fmt.Fprintln(cmd.OutOrStdout(), pathOrRef) // nolint:errcheck return nil } return err } fmt.Fprintf(cmd.OutOrStdout(), "%s@%s\n", res.Name, res.Ref) // nolint:errcheck return nil } 07070100000018000081A400000000000000000000000166CD8D3200000AAE000000000000000000000000000000000000002200000000frizbee-0.1.2/cmd/actions/list.go// // Copyright 2024 Stacklok, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package actions import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "strconv" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" "github.com/stacklok/frizbee/internal/cli" "github.com/stacklok/frizbee/pkg/replacer" "github.com/stacklok/frizbee/pkg/utils/config" ) // CmdList represents the one sub-command func CmdList() *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists the used github actions", Long: `This utility lists all the github actions used in the workflows Example: frizbee action list .github/workflows `, Aliases: []string{"ls"}, RunE: list, SilenceUsage: true, Args: cobra.MaximumNArgs(1), } cli.DeclareFrizbeeFlags(cmd, true) return cmd } func list(cmd *cobra.Command, args []string) error { // Set the default directory if not provided dir := ".github/workflows" if len(args) > 0 { dir = args[0] } dir = filepath.Clean(dir) if !cli.IsPath(dir) { return errors.New("the provided argument is not a path") } // Extract the CLI flags from the cobra command cliFlags, err := cli.NewHelper(cmd) if err != nil { return err } // Set up the config cfg, err := config.FromCommand(cmd) if err != nil { return err } // Create a new replacer r := replacer.NewGitHubActionsReplacer(cfg). WithUserRegex(cliFlags.Regex). WithGitHubClientFromToken(os.Getenv(cli.GitHubTokenEnvKey)) // List the references in the directory res, err := r.ListPath(dir) if err != nil { return err } output := cmd.Flag("output").Value.String() switch output { case "json": jsonBytes, err := json.MarshalIndent(res.Entities, "", " ") if err != nil { return err } jsonString := string(jsonBytes) fmt.Fprintln(cmd.OutOrStdout(), jsonString) // nolint:errcheck return nil case "table": table := tablewriter.NewWriter(cmd.OutOrStdout()) table.SetHeader([]string{"No", "Type", "Name", "Ref"}) for i, a := range res.Entities { table.Append([]string{strconv.Itoa(i + 1), a.Type, a.Name, a.Ref}) } table.Render() return nil default: return fmt.Errorf("unknown output format: %s", output) } } 07070100000019000041ED00000000000000000000000266CD8D3200000000000000000000000000000000000000000000001800000000frizbee-0.1.2/cmd/image0707010000001A000081A400000000000000000000000166CD8D3200000AD4000000000000000000000000000000000000002100000000frizbee-0.1.2/cmd/image/image.go// // Copyright 2024 Stacklok, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package image provides command-line utilities to work with container images. package image import ( "errors" "fmt" "path/filepath" "github.com/spf13/cobra" "github.com/stacklok/frizbee/internal/cli" "github.com/stacklok/frizbee/pkg/interfaces" "github.com/stacklok/frizbee/pkg/replacer" "github.com/stacklok/frizbee/pkg/utils/config" ) // CmdContainerImage represents the containers command func CmdContainerImage() *cobra.Command { cmd := &cobra.Command{ Use: "image", Short: "Replace container image references with checksums", Long: `This utility replaces tag or branch references in yaml/yml files with the latest commit hash of the referenced tag or branch. Example: $ frizbee image <path-to-yaml-files> or <ghcr.io/stacklok/minder/server:latest> This will replace all tag or branch references in all yaml files for the given directory. `, RunE: replaceCmd, SilenceUsage: true, Aliases: []string{"containerimage", "dockercompose", "compose"}, // backwards compatibility Args: cobra.ExactArgs(1), } // flags cli.DeclareFrizbeeFlags(cmd, false) // sub-commands cmd.AddCommand(CmdList()) return cmd } func replaceCmd(cmd *cobra.Command, args []string) error { // Extract the CLI flags from the cobra command cliFlags, err := cli.NewHelper(cmd) if err != nil { return err } // Set up the config cfg, err := config.FromCommand(cmd) if err != nil { return err } // Create a new replacer r := replacer.NewContainerImagesReplacer(cfg). WithUserRegex(cliFlags.Regex) if cli.IsPath(args[0]) { dir := filepath.Clean(args[0]) // Replace the tags in the directory res, err := r.ParsePath(cmd.Context(), dir) if err != nil { return err } // Process the output files return cliFlags.ProcessOutput(dir, res.Processed, res.Modified) } // Replace the passed reference res, err := r.ParseString(cmd.Context(), args[0]) if err != nil { if errors.Is(err, interfaces.ErrReferenceSkipped) { fmt.Fprintln(cmd.OutOrStdout(), args[0]) // nolint:errcheck return nil } return err } fmt.Fprintf(cmd.OutOrStdout(), "%s@%s\n", res.Name, res.Ref) // nolint:errcheck return nil } 0707010000001B000081A400000000000000000000000166CD8D32000009F9000000000000000000000000000000000000002000000000frizbee-0.1.2/cmd/image/list.go// // Copyright 2024 Stacklok, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package image import ( "encoding/json" "errors" "fmt" "path/filepath" "strconv" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" "github.com/stacklok/frizbee/internal/cli" "github.com/stacklok/frizbee/pkg/replacer" "github.com/stacklok/frizbee/pkg/utils/config" ) // CmdList represents the one sub-command func CmdList() *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists the used container images", Long: `This utility lists all container images used in the files in the directory Example: frizbee image list <path> `, Aliases: []string{"ls"}, RunE: list, SilenceUsage: true, Args: cobra.ExactArgs(1), } cli.DeclareFrizbeeFlags(cmd, true) return cmd } func list(cmd *cobra.Command, args []string) error { dir := filepath.Clean(args[0]) if !cli.IsPath(dir) { return errors.New("the provided argument is not a path") } // Extract the CLI flags from the cobra command cliFlags, err := cli.NewHelper(cmd) if err != nil { return err } // Set up the config cfg, err := config.FromCommand(cmd) if err != nil { return err } // Create a new replacer r := replacer.NewContainerImagesReplacer(cfg). WithUserRegex(cliFlags.Regex) // List the references in the directory res, err := r.ListPath(dir) if err != nil { return err } output := cmd.Flag("output").Value.String() switch output { case "json": jsonBytes, err := json.MarshalIndent(res.Entities, "", " ") if err != nil { return err } jsonString := string(jsonBytes) fmt.Fprintln(cmd.OutOrStdout(), jsonString) // nolint:errcheck return nil case "table": table := tablewriter.NewWriter(cmd.OutOrStdout()) table.SetHeader([]string{"No", "Type", "Name", "Ref"}) for i, a := range res.Entities { table.Append([]string{strconv.Itoa(i + 1), a.Type, a.Name, a.Ref}) } table.Render() return nil default: return fmt.Errorf("unknown output format: %s", output) } } 0707010000001C000081A400000000000000000000000166CD8D32000007D4000000000000000000000000000000000000001A00000000frizbee-0.1.2/cmd/root.go// // Copyright 2023 Stacklok, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package cmd provides the frizbee command line interface. package cmd import ( "context" "fmt" "os" "github.com/spf13/cobra" "github.com/stacklok/frizbee/cmd/actions" "github.com/stacklok/frizbee/cmd/image" "github.com/stacklok/frizbee/cmd/version" "github.com/stacklok/frizbee/pkg/utils/config" ) // Execute runs the root command. func Execute() { var rootCmd = &cobra.Command{ Use: "frizbee", Short: "frizbee is a tool you may throw a tag at and it comes back with a checksum", PersistentPreRunE: prerun, } rootCmd.PersistentFlags().StringP("config", "c", ".frizbee.yml", "config file (default is .frizbee.yml)") rootCmd.AddCommand(actions.CmdGHActions()) rootCmd.AddCommand(image.CmdContainerImage()) rootCmd.AddCommand(version.CmdVersion()) if err := rootCmd.ExecuteContext(context.Background()); err != nil { os.Exit(1) } } func prerun(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() cfg, err := readConfig(cmd) if err != nil { return fmt.Errorf("failed to read config: %w", err) } ctx = context.WithValue(ctx, config.ContextConfigKey, cfg) cmd.SetContext(ctx) return nil } func readConfig(cmd *cobra.Command) (*config.Config, error) { configFile, err := cmd.Flags().GetString("config") if err != nil { return nil, fmt.Errorf("failed to get config file: %w", err) } return config.ParseConfigFile(configFile) } 0707010000001D000041ED00000000000000000000000266CD8D3200000000000000000000000000000000000000000000001A00000000frizbee-0.1.2/cmd/version0707010000001E000081A400000000000000000000000166CD8D3200000440000000000000000000000000000000000000002500000000frizbee-0.1.2/cmd/version/version.go// // Copyright 2024 Stacklok, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package version adds a version command. package version import ( "github.com/spf13/cobra" "github.com/stacklok/frizbee/internal/cli" ) // CmdVersion is the Cobra command for the version command. func CmdVersion() *cobra.Command { return &cobra.Command{ Use: "version", Short: "Print frizbee CLI version", Long: "The frizbee version command prints the version of the frizbee CLI.", Run: func(cmd *cobra.Command, _ []string) { cmd.Println(cli.VerboseCLIVersion) }, } } 0707010000001F000081A400000000000000000000000166CD8D32000006BA000000000000000000000000000000000000001500000000frizbee-0.1.2/go.modmodule github.com/stacklok/frizbee go 1.22.5 require ( github.com/deckarep/golang-set/v2 v2.6.0 github.com/go-git/go-billy/v5 v5.5.0 github.com/google/go-containerregistry v0.20.2 github.com/google/go-github/v61 v61.0.0 github.com/moby/buildkit v0.15.2 github.com/olekukonko/tablewriter v0.0.5 github.com/puzpuzpuz/xsync v1.5.2 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 golang.org/x/sync v0.8.0 gopkg.in/h2non/gock.v1 v1.1.2 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect github.com/containerd/typeurl/v2 v2.1.1 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/cli v27.1.1+incompatible // indirect github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/vbatts/tar-split v0.11.5 // indirect golang.org/x/sys v0.22.0 // indirect google.golang.org/protobuf v1.33.0 // indirect ) 07070100000020000081A400000000000000000000000166CD8D3200002F8D000000000000000000000000000000000000001500000000frizbee-0.1.2/go.sumgithub.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU= github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk= github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE= github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo= github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8= github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go= github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/moby/buildkit v0.15.2 h1:DnONr0AoceTWyv+plsQ7IhkSaj+6o0WyoaxYPyTFIxs= github.com/moby/buildkit v0.15.2/go.mod h1:Yis8ZMUJTHX9XhH9zVyK2igqSHV3sxi3UN0uztZocZk= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/puzpuzpuz/xsync v1.5.2 h1:yRAP4wqSOZG+/4pxJ08fPTwrfL0IzE/LKQ/cw509qGY= github.com/puzpuzpuz/xsync v1.5.2/go.mod h1:K98BYhX3k1dQ2M63t1YNVDanbwUPmBCAhNmVrrxfiGg= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= 07070100000021000041ED00000000000000000000000266CD8D3200000000000000000000000000000000000000000000001700000000frizbee-0.1.2/internal07070100000022000041ED00000000000000000000000266CD8D3200000000000000000000000000000000000000000000001B00000000frizbee-0.1.2/internal/cli07070100000023000081A400000000000000000000000166CD8D320000174D000000000000000000000000000000000000002200000000frizbee-0.1.2/internal/cli/cli.go// // Copyright 2024 Stacklok, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package cli provides utilities to work with the command-line interface. package cli import ( "fmt" "io" "os" "path/filepath" "runtime/debug" "strings" "text/template" "github.com/go-git/go-billy/v5/osfs" "github.com/spf13/cobra" ) const ( // UserAgent is the user agent string used by frizbee. // // TODO (jaosorior): Add version information to this. UserAgent = "frizbee" // GitHubTokenEnvKey is the environment variable key for the GitHub token //nolint:gosec // This is not a hardcoded credential GitHubTokenEnvKey = "GITHUB_TOKEN" // TokenHelpText is the help text for the GitHub token TokenHelpText = "NOTE: It's recommended to set the " + GitHubTokenEnvKey + " environment variable given that GitHub has tighter rate limits on anonymous calls." verboseTemplate = `Version: {{ .Version }} Go Version: {{.GoVersion}} Git Commit: {{.Commit}} Commit Date: {{.Time}} OS/Arch: {{.OS}}/{{.Arch}} Dirty: {{.Modified}} ` ) // Helper is a common struct for implementing a CLI command that replaces // files. type Helper struct { DryRun bool Quiet bool ErrOnModified bool Regex string Cmd *cobra.Command } type versionInfo struct { Version string GoVersion string Time string Commit string OS string Arch string Modified bool } var ( // CLIVersion is the version of the frizbee CLI. // nolint: gochecknoglobals CLIVersion = "dev" // VerboseCLIVersion is the verbose version of the frizbee CLI. // nolint: gochecknoglobals VerboseCLIVersion = "" ) // nolint:gochecknoinits func init() { buildinfo, ok := debug.ReadBuildInfo() if !ok { return } var vinfo versionInfo vinfo.Version = CLIVersion vinfo.GoVersion = buildinfo.GoVersion for _, kv := range buildinfo.Settings { switch kv.Key { case "vcs.time": vinfo.Time = kv.Value case "vcs.revision": vinfo.Commit = kv.Value case "vcs.modified": vinfo.Modified = kv.Value == "true" case "GOOS": vinfo.OS = kv.Value case "GOARCH": vinfo.Arch = kv.Value } } VerboseCLIVersion = vinfo.String() } func (vvs *versionInfo) String() string { stringBuilder := &strings.Builder{} tmpl := template.Must(template.New("version").Parse(verboseTemplate)) err := tmpl.Execute(stringBuilder, vvs) if err != nil { panic(err) } return stringBuilder.String() } // NewHelper creates a new CLI Helper struct. func NewHelper(cmd *cobra.Command) (*Helper, error) { dryRun, err := cmd.Flags().GetBool("dry-run") if err != nil { return nil, fmt.Errorf("failed to get dry-run flag: %w", err) } errOnModified, err := cmd.Flags().GetBool("error") if err != nil { return nil, fmt.Errorf("failed to get error flag: %w", err) } quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return nil, fmt.Errorf("failed to get quiet flag: %w", err) } regex, err := cmd.Flags().GetString("regex") if err != nil { return nil, fmt.Errorf("failed to get regex flag: %w", err) } return &Helper{ Cmd: cmd, DryRun: dryRun, ErrOnModified: errOnModified, Quiet: quiet, Regex: regex, }, nil } // DeclareFrizbeeFlags declares the flags common to all replacer commands. func DeclareFrizbeeFlags(cmd *cobra.Command, enableOutput bool) { cmd.Flags().BoolP("dry-run", "n", false, "don't modify files") cmd.Flags().BoolP("quiet", "q", false, "don't print anything") cmd.Flags().BoolP("error", "e", false, "exit with error code if any file is modified") cmd.Flags().StringP("regex", "r", "", "regex to match artifact references") cmd.Flags().StringP("platform", "p", "", "platform to match artifact references, e.g. linux/amd64") if enableOutput { cmd.Flags().StringP("output", "o", "table", "output format. Can be 'json' or 'table'") } } // Logf logs the given message to the given command's stderr if the command is // not quiet. func (r *Helper) Logf(format string, args ...interface{}) { if !r.Quiet { fmt.Fprintf(r.Cmd.ErrOrStderr(), format, args...) // nolint:errcheck } } // ProcessOutput processes the given output files. // If the command is quiet, the output is discarded. // If the command is a dry run, the output is written to the command's stdout. // Otherwise, the output is written to the given filesystem. func (r *Helper) ProcessOutput(path string, processed []string, modified map[string]string) error { basedir := filepath.Dir(path) bfs := osfs.New(basedir, osfs.WithBoundOS()) var out io.Writer for _, path := range processed { if !r.Quiet { r.Logf("Processed: %s\n", path) } } for path, content := range modified { if r.Quiet { out = io.Discard } else if r.DryRun { out = r.Cmd.OutOrStdout() } else { f, err := bfs.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { return fmt.Errorf("failed to open file %s: %w", path, err) } defer func() { if err := f.Close(); err != nil { fmt.Fprintf(r.Cmd.ErrOrStderr(), "failed to close file %s: %v", path, err) // nolint:errcheck } }() out = f } if !r.Quiet { r.Logf("Modified: %s\n", path) } _, err := fmt.Fprintf(out, "%s", content) if err != nil { return fmt.Errorf("failed to write to file %s: %w", path, err) } } return nil } // IsPath returns true if the given path is a file or directory. func IsPath(pathOrRef string) bool { _, err := os.Stat(pathOrRef) return err == nil } 07070100000024000081A400000000000000000000000166CD8D320000131C000000000000000000000000000000000000002700000000frizbee-0.1.2/internal/cli/cli_test.gopackage cli import ( "path/filepath" "strings" "testing" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/memfs" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) func TestNewHelper(t *testing.T) { t.Parallel() testCases := []struct { name string cmdArgs []string expected *Helper expectedError bool }{ { name: "ValidFlags", cmdArgs: []string{"--dry-run", "--quiet", "--error", "--regex", "test"}, expected: &Helper{ DryRun: true, Quiet: true, ErrOnModified: true, Regex: "test", }, expectedError: false, }, { name: "MissingFlags", cmdArgs: []string{}, expected: &Helper{}, expectedError: false, }, { name: "InvalidFlags", cmdArgs: []string{"--nonexistent"}, expected: nil, expectedError: true, }, } for _, tt := range testCases { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() cmd := &cobra.Command{} DeclareFrizbeeFlags(cmd, true) cmd.SetArgs(tt.cmdArgs) if tt.expectedError { assert.Error(t, cmd.Execute()) return } assert.NoError(t, cmd.Execute()) helper, err := NewHelper(cmd) if tt.expectedError { assert.Error(t, err) assert.Nil(t, helper) } else { assert.NoError(t, err) assert.NotNil(t, helper) assert.Equal(t, tt.expected.DryRun, helper.DryRun) assert.Equal(t, tt.expected.Quiet, helper.Quiet) assert.Equal(t, tt.expected.ErrOnModified, helper.ErrOnModified) assert.Equal(t, tt.expected.Regex, helper.Regex) } }) } } func TestProcessOutput(t *testing.T) { t.Parallel() testCases := []struct { name string helper *Helper path string processed []string modified map[string]string expectedOutput string expectError bool }{ { name: "QuietMode", helper: &Helper{ Quiet: true, Cmd: &cobra.Command{}, }, path: "test/path", processed: []string{"file1.txt", "file2.txt"}, modified: map[string]string{"file1.txt": "new content"}, expectedOutput: "", expectError: false, }, { name: "DryRunMode", helper: &Helper{ Quiet: false, DryRun: true, Cmd: &cobra.Command{}, }, path: "test/path", processed: []string{"file1.txt"}, modified: map[string]string{"file1.txt": "new content"}, expectedOutput: "Processed: file1.txt\nModified: file1.txt\nnew content", expectError: false, }, { name: "ErrorOpeningFile", helper: &Helper{ Quiet: false, Cmd: &cobra.Command{}, }, path: "invalid/path", modified: map[string]string{"invalid/path": "new content"}, expectedOutput: "", expectError: true, }, } for _, tt := range testCases { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() // Set up command output var output strings.Builder tt.helper.Cmd.SetOut(&output) tt.helper.Cmd.SetErr(&output) // Create in-memory filesystem and add files fs := memfs.New() for path, content := range tt.modified { dir := filepath.Join(tt.path, filepath.Dir(path)) assert.NoError(t, fs.MkdirAll(dir, 0755)) file, err := fs.Create(filepath.Join(tt.path, path)) if err == nil { _, _ = file.Write([]byte(content)) assert.NoError(t, file.Close()) } } // Process the output using the in-memory filesystem err := tt.helper.ProcessOutput(tt.path, tt.processed, tt.modified) if tt.expectError { assert.Error(t, err) } else { assert.NoError(t, err) assert.Contains(t, output.String(), tt.expectedOutput) } }) } } func TestIsPath(t *testing.T) { t.Parallel() testCases := []struct { name string setup func(fs billy.Filesystem) path string expected bool }{ { name: "ExistingFile", setup: func(fs billy.Filesystem) { file, _ := fs.Create("testfile.txt") assert.NoError(t, file.Close()) }, path: "testfile.txt", expected: true, }, { name: "NonExistentFile", setup: func(_ billy.Filesystem) {}, path: "nonexistent.txt", expected: false, }, { name: "ExistingDirectory", setup: func(fs billy.Filesystem) { assert.NoError(t, fs.MkdirAll("testdir", 0755)) }, path: "testdir", expected: true, }, { name: "NonExistentDirectory", setup: func(_ billy.Filesystem) {}, path: "nonexistentdir", expected: false, }, } for _, tt := range testCases { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() // Use in-memory filesystem for testing fs := memfs.New() tt.setup(fs) // Check if the path exists in the in-memory filesystem _, err := fs.Stat(tt.path) result := err == nil assert.Equal(t, tt.expected, result) }) } } 07070100000025000041ED00000000000000000000000266CD8D3200000000000000000000000000000000000000000000002000000000frizbee-0.1.2/internal/traverse07070100000026000081A400000000000000000000000166CD8D3200001229000000000000000000000000000000000000002C00000000frizbee-0.1.2/internal/traverse/traverse.go// // Copyright 2024 Stacklok, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package traverse provides utilities to traverse directories. package traverse import ( "fmt" "io/fs" "os" "path/filepath" "strings" "github.com/go-git/go-billy/v5" ) // GhwFunc is a function that gets called with each file in a GitHub Actions workflow // directory. It receives the path to the file. type GhwFunc func(path string) error // FuncTraverse is a function that gets called with each file in a directory. type FuncTraverse func(path string, info fs.FileInfo) error // YamlDockerfiles traverses all yaml/yml in the given directory // and calls the given function with each workflow. func YamlDockerfiles(bfs billy.Filesystem, base string, fun GhwFunc) error { return Traverse(bfs, base, func(path string, info fs.FileInfo) error { if !isYAMLOrDockerfile(info) { return nil } if err := fun(path); err != nil { return fmt.Errorf("failed to process file %s: %w", path, err) } return nil }) } // Traverse traverses the given directory and calls the given function with each file. func Traverse(bfs billy.Filesystem, base string, fun FuncTraverse) error { return Walk(bfs, base, func(path string, info fs.FileInfo, err error) error { if err != nil { return nil } return fun(path, info) }) } // isYAMLOrDockerfile returns true if the given file is a YAML or Dockerfile. func isYAMLOrDockerfile(info fs.FileInfo) bool { // Skip if not a file if info.IsDir() { return false } // Filter out files that are not yml, yaml or dockerfiles if strings.HasSuffix(info.Name(), ".yml") || strings.HasSuffix(info.Name(), ".yaml") || strings.Contains(strings.ToLower(info.Name()), "dockerfile") { return true } return false } // walk recursively descends path, calling walkFn // adapted from https://golang.org/src/path/filepath/path.go func walk(bfs billy.Filesystem, path string, info os.FileInfo, walkFn filepath.WalkFunc) error { if !info.IsDir() { return walkFn(path, info, nil) } names, err := readDirNames(bfs, path) err1 := walkFn(path, info, err) // If err != nil, walk can't walk into this directory. // err1 != nil means walkFn want walk to skip this directory or stop walking. // Therefore, if one of err and err1 isn't nil, walk will return. if err != nil || err1 != nil { // The caller's behavior is controlled by the return value, which is decided // by walkFn. walkFn may ignore err and return nil. // If walkFn returns SkipDir, it will be handled by the caller. // So walk should return whatever walkFn returns. return err1 } for _, name := range names { filename := filepath.Join(path, name) fileInfo, err := bfs.Lstat(filename) if err != nil { if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir { return err } } else { err = walk(bfs, filename, fileInfo, walkFn) if err != nil { if !fileInfo.IsDir() || err != filepath.SkipDir { return err } } } } return nil } // Walk walks the file tree rooted at root, calling fn for each file or // directory in the tree, including root. All errors that arise visiting files // and directories are filtered by fn: see the WalkFunc documentation for // details. // // The files are walked in lexical order, which makes the output deterministic // but requires Walk to read an entire directory into memory before proceeding // to walk that directory. Walk does not follow symbolic links. // // Function adapted from https://github.com/golang/go/blob/3b770f2ccb1fa6fecc22ea822a19447b10b70c5c/src/path/filepath/path.go#L500 func Walk(bfs billy.Filesystem, root string, walkFn filepath.WalkFunc) error { info, err := bfs.Lstat(root) if err != nil { err = walkFn(root, nil, err) } else { err = walk(bfs, root, info, walkFn) } if err == filepath.SkipDir { return nil } return err } func readDirNames(bfs billy.Filesystem, dir string) ([]string, error) { files, err := bfs.ReadDir(dir) if err != nil { return nil, err } var names []string for _, file := range files { names = append(names, file.Name()) } return names, nil } 07070100000027000081A400000000000000000000000166CD8D32000014F6000000000000000000000000000000000000003100000000frizbee-0.1.2/internal/traverse/traverse_test.gopackage traverse import ( "errors" "os" "testing" "time" "github.com/go-git/go-billy/v5/memfs" "github.com/stretchr/testify/assert" ) func TestYamlDockerfiles(t *testing.T) { t.Parallel() testCases := []struct { name string fsContent map[string]string baseDir string expected []string expectError bool }{ { name: "NoYAMLOrDockerfile", fsContent: map[string]string{ "base/file.txt": "content", }, baseDir: "base", expected: []string{}, expectError: false, }, { name: "WithYAMLFiles", fsContent: map[string]string{ "base/file.yml": "content", "base/file.yaml": "content", "base/not_included.txt": "content", }, baseDir: "base", expected: []string{ "base/file.yml", "base/file.yaml", }, expectError: false, }, { name: "WithDockerfiles", fsContent: map[string]string{ "base/Dockerfile": "content", "base/nested/dockerfile": "content", "base/not_included.txt": "content", }, baseDir: "base", expected: []string{ "base/Dockerfile", "base/nested/dockerfile", }, expectError: false, }, { name: "MixedFiles", fsContent: map[string]string{ "base/file.yml": "content", "base/Dockerfile": "content", "base/nested/file.yaml": "content", "base/nested/dockerfile": "content", "base/not_included.txt": "content", }, baseDir: "base", expected: []string{ "base/file.yml", "base/Dockerfile", "base/nested/file.yaml", "base/nested/dockerfile", }, expectError: false, }, { name: "ErrorInProcessingFile", fsContent: map[string]string{ "base/file.yml": "content", }, baseDir: "base", expectError: true, }, } for _, tt := range testCases { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() fs := memfs.New() for name, content := range tt.fsContent { f, _ := fs.Create(name) _, _ = f.Write([]byte(content)) assert.NoError(t, f.Close()) } var processedFiles []string err := YamlDockerfiles(fs, tt.baseDir, func(path string) error { if tt.expectError { return errors.New("error in processing file") } processedFiles = append(processedFiles, path) return nil }) if tt.expectError { assert.Error(t, err) } else { assert.NoError(t, err) assert.ElementsMatch(t, tt.expected, processedFiles) } }) } } func TestTraverse(t *testing.T) { t.Parallel() testCases := []struct { name string fsContent map[string]string baseDir string expected []string expectError bool }{ { name: "TraverseFiles", fsContent: map[string]string{ "base/file1.txt": "content", "base/file2.txt": "content", "base/nested/file": "content", }, baseDir: "base", expected: []string{ "base", "base/file1.txt", "base/file2.txt", "base/nested", "base/nested/file", }, expectError: false, }, { name: "TraverseWithError", fsContent: map[string]string{ "base/file.txt": "content", }, baseDir: "base", expectError: true, }, } for _, tt := range testCases { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() fs := memfs.New() for name, content := range tt.fsContent { f, _ := fs.Create(name) _, _ = f.Write([]byte(content)) assert.NoError(t, f.Close()) } var processedFiles []string err := Traverse(fs, tt.baseDir, func(path string, _ os.FileInfo) error { if tt.expectError { return errors.New("error in traversing file") } processedFiles = append(processedFiles, path) return nil }) if tt.expectError { assert.Error(t, err) } else { assert.NoError(t, err) assert.ElementsMatch(t, tt.expected, processedFiles) } }) } } func TestIsYAMLOrDockerfile(t *testing.T) { t.Parallel() testCases := []struct { name string fileName string isDir bool expected bool }{ { name: "YAMLFile", fileName: "config.yaml", isDir: false, expected: true, }, { name: "YMLFile", fileName: "config.yml", isDir: false, expected: true, }, { name: "Dockerfile", fileName: "Dockerfile", isDir: false, expected: true, }, { name: "dockerfile", fileName: "dockerfile", isDir: false, expected: true, }, { name: "NonYAMLOrDockerfile", fileName: "config.txt", isDir: false, expected: false, }, { name: "Directory", fileName: "config", isDir: true, expected: false, }, } for _, tt := range testCases { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() info := &fileInfoMock{ name: tt.fileName, dir: tt.isDir, } result := isYAMLOrDockerfile(info) assert.Equal(t, tt.expected, result) }) } } // fileInfoMock is a mock implementation of os.FileInfo for testing. type fileInfoMock struct { name string dir bool } func (f *fileInfoMock) Name() string { return f.name } func (_ *fileInfoMock) Size() int64 { return 0 } func (_ *fileInfoMock) Mode() os.FileMode { return 0 } func (_ *fileInfoMock) ModTime() time.Time { return time.Time{} } func (f *fileInfoMock) IsDir() bool { return f.dir } func (_ *fileInfoMock) Sys() interface{} { return nil } 07070100000028000081A400000000000000000000000166CD8D3200000089000000000000000000000000000000000000001600000000frizbee-0.1.2/main.go/* Copyright © 2023 NAME HERE <EMAIL ADDRESS> */ package main import "github.com/stacklok/frizbee/cmd" func main() { cmd.Execute() } 07070100000029000041ED00000000000000000000000266CD8D3200000000000000000000000000000000000000000000001200000000frizbee-0.1.2/pkg0707010000002A000041ED00000000000000000000000266CD8D3200000000000000000000000000000000000000000000001D00000000frizbee-0.1.2/pkg/interfaces0707010000002B000081A400000000000000000000000166CD8D3200000747000000000000000000000000000000000000002B00000000frizbee-0.1.2/pkg/interfaces/interfaces.go// // Copyright 2024 Stacklok, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package interfaces provides interfaces for the frizbee package. package interfaces import ( "context" "errors" "net/http" "github.com/stacklok/frizbee/pkg/utils/config" "github.com/stacklok/frizbee/pkg/utils/store" ) var ( // ErrReferenceSkipped is returned when the reference is skipped. ErrReferenceSkipped = errors.New("reference skipped") ) // EntityRef represents an action reference. type EntityRef struct { Name string `json:"name"` Ref string `json:"ref"` Type string `json:"type"` Tag string `json:"tag"` Prefix string `json:"prefix"` } // Parser is an interface to replace references with digests type Parser interface { SetCache(cache store.RefCacher) SetRegex(regex string) GetRegex() string Replace(ctx context.Context, matchedLine string, restIf REST, cfg config.Config) (*EntityRef, error) ConvertToEntityRef(reference string) (*EntityRef, error) } // The REST interface allows to wrap clients to talk to remotes // When talking to GitHub, wrap a github client to provide this interface type REST interface { // NewRequest creates an HTTP request. NewRequest(method, url string, body any) (*http.Request, error) // Do executes an HTTP request. Do(ctx context.Context, req *http.Request) (*http.Response, error) } 0707010000002C000041ED00000000000000000000000266CD8D3200000000000000000000000000000000000000000000001B00000000frizbee-0.1.2/pkg/replacer0707010000002D000041ED00000000000000000000000266CD8D3200000000000000000000000000000000000000000000002300000000frizbee-0.1.2/pkg/replacer/actions0707010000002E000081A400000000000000000000000166CD8D3200002A36000000000000000000000000000000000000002E00000000frizbee-0.1.2/pkg/replacer/actions/actions.go// // Copyright 2024 Stacklok, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package actions provides utilities to work with GitHub Actions. package actions import ( "context" "encoding/json" "errors" "fmt" "net/http" "net/url" "slices" "strings" "github.com/google/go-github/v61/github" "github.com/stacklok/frizbee/pkg/interfaces" "github.com/stacklok/frizbee/pkg/replacer/image" "github.com/stacklok/frizbee/pkg/utils/config" "github.com/stacklok/frizbee/pkg/utils/store" ) const ( prefixUses = "uses: " prefixDocker = "docker://" // GitHubActionsRegex is regular expression pattern to match GitHub Actions usage GitHubActionsRegex = `uses:\s*[^\s]+/[^\s]+@[^\s]+|uses:\s*docker://[^\s]+:[^\s]+` // ReferenceType is the type of the reference ReferenceType = "action" ) var ( // ErrInvalidAction is returned when parsing the action fails. ErrInvalidAction = errors.New("invalid action") // ErrInvalidActionReference is returned when parsing the action reference fails. ErrInvalidActionReference = errors.New("action reference is not a tag nor branch") ) // Parser is a struct to replace action references with digests type Parser struct { regex string cache store.RefCacher } // New creates a new Parser func New() *Parser { return &Parser{ regex: GitHubActionsRegex, cache: store.NewRefCacher(), } } // SetCache returns the regular expression pattern to match GitHub Actions usage func (p *Parser) SetCache(cache store.RefCacher) { p.cache = cache } // SetRegex returns the regular expression pattern to match GitHub Actions usage func (p *Parser) SetRegex(regex string) { p.regex = regex } // GetRegex returns the regular expression pattern to match GitHub Actions usage func (p *Parser) GetRegex() string { return p.regex } // Replace replaces the action reference with the digest func (p *Parser) Replace( ctx context.Context, matchedLine string, restIf interfaces.REST, cfg config.Config, ) (*interfaces.EntityRef, error) { var err error var actionRef *interfaces.EntityRef hasUsesPrefix := false // Trim the uses prefix if strings.HasPrefix(matchedLine, prefixUses) { matchedLine = strings.TrimPrefix(matchedLine, prefixUses) hasUsesPrefix = true } // Determine if the action reference has a docker prefix if strings.HasPrefix(matchedLine, prefixDocker) { actionRef, err = p.replaceDocker(ctx, matchedLine, restIf, cfg) } else { actionRef, err = p.replaceAction(ctx, matchedLine, restIf, cfg) } if err != nil { return nil, err } // Add back the uses prefix if hasUsesPrefix { actionRef.Prefix = fmt.Sprintf("%s%s", prefixUses, actionRef.Prefix) } // Return the new action reference return actionRef, nil } func (p *Parser) replaceAction( ctx context.Context, matchedLine string, restIf interfaces.REST, cfg config.Config, ) (*interfaces.EntityRef, error) { // If the value is a local path or should be excluded, skip it if isLocal(matchedLine) || shouldExclude(&cfg.GHActions, matchedLine) { return nil, fmt.Errorf("%w: %s", interfaces.ErrReferenceSkipped, matchedLine) } // Parse the action reference act, ref, err := ParseActionReference(matchedLine) if err != nil { return nil, fmt.Errorf("failed to parse action reference '%s': %w", matchedLine, err) } // Check if the parsed reference should be excluded if shouldExclude(&cfg.GHActions, act) { return nil, fmt.Errorf("%w: %s", interfaces.ErrReferenceSkipped, matchedLine) } var sum string // Check if we have a cache if p.cache != nil { // Check if we have a cached value if val, ok := p.cache.Load(matchedLine); ok { sum = val } else { // Get the checksum for the action reference sum, err = GetChecksum(ctx, cfg.GHActions, restIf, act, ref) if err != nil { return nil, fmt.Errorf("failed to get checksum for action '%s': %w", matchedLine, err) } // Store the checksum in the cache p.cache.Store(matchedLine, sum) } } else { // Get the checksum for the action reference sum, err = GetChecksum(ctx, cfg.GHActions, restIf, act, ref) if err != nil { return nil, fmt.Errorf("failed to get checksum for action '%s': %w", matchedLine, err) } } // Compare the digest with the reference and return the original reference if they already match if ref == sum { return nil, fmt.Errorf("image already referenced by digest: %s %w", matchedLine, interfaces.ErrReferenceSkipped) } return &interfaces.EntityRef{ Name: act, Ref: sum, Type: ReferenceType, Tag: ref, }, nil } func (p *Parser) replaceDocker( ctx context.Context, matchedLine string, _ interfaces.REST, cfg config.Config, ) (*interfaces.EntityRef, error) { // Trim the docker prefix trimmedRef := strings.TrimPrefix(matchedLine, prefixDocker) // If the value is a local path or should be excluded, skip it if isLocal(trimmedRef) || shouldExclude(&cfg.GHActions, trimmedRef) { return nil, fmt.Errorf("%w: %s", interfaces.ErrReferenceSkipped, matchedLine) } // Get the digest of the docker:// image reference actionRef, err := image.GetImageDigestFromRef(ctx, trimmedRef, cfg.Platform, p.cache) if err != nil { return nil, err } // Check if the parsed reference should be excluded if shouldExclude(&cfg.GHActions, actionRef.Name) { return nil, fmt.Errorf("%w: %s", interfaces.ErrReferenceSkipped, matchedLine) } // Add back the docker prefix if strings.HasPrefix(matchedLine, prefixDocker) { actionRef.Prefix = fmt.Sprintf("%s%s", prefixDocker, actionRef.Prefix) } return actionRef, nil } // ConvertToEntityRef converts an action reference to an EntityRef func (_ *Parser) ConvertToEntityRef(reference string) (*interfaces.EntityRef, error) { reference = strings.TrimPrefix(reference, prefixUses) refType := ReferenceType separator := "@" // Update the separator in case this is a docker reference with a digest if strings.Contains(reference, prefixDocker) { reference = strings.TrimPrefix(reference, prefixDocker) if !strings.Contains(reference, separator) && strings.Contains(reference, ":") { separator = ":" } refType = image.ReferenceType } frags := strings.Split(reference, separator) if len(frags) != 2 { return nil, fmt.Errorf("invalid action reference: %s", reference) } return &interfaces.EntityRef{ Name: frags[0], Ref: frags[1], Type: refType, }, nil } // isLocal returns true if the input is a local path. func isLocal(input string) bool { return strings.HasPrefix(input, "./") || strings.HasPrefix(input, "../") } func shouldExclude(cfg *config.GHActions, input string) bool { for _, e := range cfg.Exclude { if e == input { return true } } return false } // ParseActionReference parses an action reference into action and reference. func ParseActionReference(input string) (action string, reference string, err error) { frags := strings.Split(input, "@") if len(frags) != 2 { return "", "", fmt.Errorf("invalid action reference: %s", input) } return frags[0], frags[1], nil } // GetChecksum returns the checksum for a given action and tag. func GetChecksum(ctx context.Context, cfg config.GHActions, restIf interfaces.REST, action, ref string) (string, error) { owner, repo, err := parseActionFragments(action) if err != nil { return "", err } // Check if we're using a checksum if isChecksum(ref) { return ref, nil } res, err := getCheckSumForTag(ctx, restIf, owner, repo, ref) if err != nil { return "", fmt.Errorf("failed to get checksum for tag: %w", err) } else if res != "" { return res, nil } // check branch if excludeBranch(cfg.Filter.ExcludeBranches, ref) { // if a branch is excluded, we won't know if it's a valid reference // but that's OK - we just won't touch that reference return "", fmt.Errorf("%w: %s", interfaces.ErrReferenceSkipped, ref) } res, err = getCheckSumForBranch(ctx, restIf, owner, repo, ref) if err != nil { return "", fmt.Errorf("failed to get checksum for branch: %w", err) } else if res != "" { return res, nil } return "", ErrInvalidActionReference } func parseActionFragments(action string) (owner string, repo string, err error) { frags := strings.Split(action, "/") // if we have more than 2 fragments, we're probably dealing with // sub-actions, so we take the first two fragments as the owner and repo if len(frags) < 2 { return "", "", fmt.Errorf("%w: '%s' reference is incorrect", ErrInvalidAction, action) } return frags[0], frags[1], nil } // isChecksum returns true if the input is a checksum. func isChecksum(ref string) bool { return len(ref) == 40 } func getCheckSumForTag(ctx context.Context, restIf interfaces.REST, owner, repo, tag string) (string, error) { path, err := url.JoinPath("repos", owner, repo, "git", "refs", "tags", tag) if err != nil { return "", fmt.Errorf("failed to join path: %w", err) } return doGetReference(ctx, restIf, path) } func getCheckSumForBranch(ctx context.Context, restIf interfaces.REST, owner, repo, branch string) (string, error) { path, err := url.JoinPath("repos", owner, repo, "git", "refs", "heads", branch) if err != nil { return "", fmt.Errorf("failed to join path: %w", err) } return doGetReference(ctx, restIf, path) } func excludeBranch(excludes []string, branch string) bool { if len(excludes) == 0 { return false } if slices.Contains(excludes, "*") { return true } return slices.Contains(excludes, branch) } func doGetReference(ctx context.Context, restIf interfaces.REST, path string) (string, error) { req, err := restIf.NewRequest(http.MethodGet, path, nil) if err != nil { return "", fmt.Errorf("cannot create REST request: %w", err) } resp, err := restIf.Do(ctx, req) if resp != nil { defer func() { _ = resp.Body.Close() }() } if err != nil && resp.StatusCode != http.StatusNotFound { return "", fmt.Errorf("failed to do API request: %w", err) } else if resp.StatusCode == http.StatusNotFound { // No error, but no tag found return "", nil } var t github.Reference err = json.NewDecoder(resp.Body).Decode(&t) if err != nil && strings.Contains(err.Error(), "cannot unmarshal array into Go value of type") { // This is a branch, not a tag return "", nil } else if err != nil { return "", fmt.Errorf("canont decode response: %w", err) } return t.GetObject().GetSHA(), nil } 0707010000002F000081A400000000000000000000000166CD8D3200002026000000000000000000000000000000000000003300000000frizbee-0.1.2/pkg/replacer/actions/actions_test.gopackage actions import ( "context" "os" "testing" "github.com/stretchr/testify/require" "github.com/stacklok/frizbee/pkg/utils/config" "github.com/stacklok/frizbee/pkg/utils/ghrest" "github.com/stacklok/frizbee/pkg/utils/store" ) func TestNewParser(t *testing.T) { t.Parallel() parser := New() require.NotNil(t, parser, "Parser should not be nil") require.Equal(t, GitHubActionsRegex, parser.regex, "Default regex should be GitHubActionsRegex") require.NotNil(t, parser.cache, "Cache should be initialized") } func TestSetCache(t *testing.T) { t.Parallel() parser := New() cache := store.NewRefCacher() parser.SetCache(cache) require.Equal(t, cache, parser.cache, "Cache should be set correctly") } func TestSetAndGetRegex(t *testing.T) { t.Parallel() parser := New() tests := []struct { name string newRegex string }{ { name: "Set and get new regex", newRegex: `new-regex`, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() parser.SetRegex(tt.newRegex) require.Equal(t, tt.newRegex, parser.GetRegex(), "Regex should be set and retrieved correctly") }) } } func TestReplaceLocalPath(t *testing.T) { t.Parallel() parser := New() ctx := context.Background() cfg := config.Config{} restIf := &ghrest.Client{} tests := []struct { name string matchedLine string }{ { name: "Replace local path", matchedLine: "./local/path", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() _, err := parser.Replace(ctx, tt.matchedLine, restIf, cfg) require.Error(t, err, "Should return error for local path") require.Contains(t, err.Error(), "reference skipped", "Error should indicate reference skipped") }) } } func TestReplaceExcludedPath(t *testing.T) { t.Parallel() parser := New() ctx := context.Background() cfg := config.Config{GHActions: config.GHActions{Filter: config.Filter{Exclude: []string{"actions/checkout"}}}} restIf := &ghrest.Client{} tests := []struct { name string matchedLine string }{ { name: "Replace excluded path", matchedLine: "uses: actions/checkout@v2", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() _, err := parser.Replace(ctx, tt.matchedLine, restIf, cfg) require.Error(t, err, "Should return error for excluded path") require.Contains(t, err.Error(), "reference skipped", "Error should indicate reference skipped") }) } } func TestConvertToEntityRef(t *testing.T) { t.Parallel() parser := New() tests := []struct { name string reference string wantErr bool }{ {"Valid action reference", "uses: actions/checkout@v2", false}, {"Valid docker reference", "docker://mydocker/image:tag", false}, {"Invalid reference format", "invalid-reference", true}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() ref, err := parser.ConvertToEntityRef(tt.reference) if tt.wantErr { require.Error(t, err, "Expected error but got none") } else { require.NoError(t, err, "Expected no error but got %v", err) require.NotNil(t, ref, "EntityRef should not be nil") } }) } } func TestIsLocal(t *testing.T) { t.Parallel() tests := []struct { name string input string want bool }{ {"Local path with ./", "./local/path", true}, {"Local path with ../", "../local/path", true}, {"Non-local path", "non/local/path", false}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() require.Equal(t, tt.want, isLocal(tt.input), "IsLocal should return correct value") }) } } func TestShouldExclude(t *testing.T) { t.Parallel() cfg := &config.GHActions{Filter: config.Filter{Exclude: []string{"actions/checkout", "actions/setup"}}} tests := []struct { name string input string want bool }{ {"Excluded path", "actions/checkout", true}, {"Non-excluded path", "actions/unknown", false}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() require.Equal(t, tt.want, shouldExclude(cfg, tt.input), "ShouldExclude should return correct value") }) } } func TestParseActionReference(t *testing.T) { t.Parallel() tests := []struct { name string input string wantAction string wantRef string wantErr bool }{ {"Valid action reference", "actions/checkout@v2", "actions/checkout", "v2", false}, {"Invalid reference format", "invalid-reference", "", "", true}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() action, ref, err := ParseActionReference(tt.input) if tt.wantErr { require.Error(t, err, "Expected error but got none") } else { require.NoError(t, err, "Expected no error but got %v", err) require.Equal(t, tt.wantAction, action, "Action should be parsed correctly") require.Equal(t, tt.wantRef, ref, "Reference should be parsed correctly") } }) } } func TestGetChecksum(t *testing.T) { t.Parallel() tok := os.Getenv("GITHUB_TOKEN") ctx := context.Background() ghcli := ghrest.NewClient(tok) tests := []struct { name string args struct{ action, ref string } want string wantErr bool }{ { name: "actions/checkout with v4.1.1", args: struct{ action, ref string }{action: "actions/checkout", ref: "v4.1.1"}, want: "b4ffde65f46336ab88eb53be808477a3936bae11", wantErr: false, }, { name: "actions/checkout with v3.6.0", args: struct{ action, ref string }{action: "actions/checkout", ref: "v3.6.0"}, want: "f43a0e5ff2bd294095638e18286ca9a3d1956744", wantErr: false, }, { name: "actions/checkout with checksum returns checksum", args: struct{ action, ref string }{action: "actions/checkout", ref: "1d96c772d19495a3b5c517cd2bc0cb401ea0529f"}, want: "1d96c772d19495a3b5c517cd2bc0cb401ea0529f", wantErr: false, }, { name: "aquasecurity/trivy-action with 0.14.0", args: struct{ action, ref string }{action: "aquasecurity/trivy-action", ref: "0.14.0"}, want: "2b6a709cf9c4025c5438138008beaddbb02086f0", wantErr: false, }, { name: "aquasecurity/trivy-action with branch returns checksum", args: struct{ action, ref string }{action: "aquasecurity/trivy-action", ref: "bump-trivy"}, want: "fb5e1b36be448e92ca98648c661bd7e9da1f1317", wantErr: false, }, { name: "actions/checkout with invalid tag returns error", args: struct{ action, ref string }{action: "actions/checkout", ref: "v4.1.1.1"}, want: "", wantErr: true, }, { name: "actions/checkout with invalid action returns error", args: struct{ action, ref string }{action: "invalid-action", ref: "v4.1.1"}, want: "", wantErr: true, }, { name: "actions/checkout with empty action returns error", args: struct{ action, ref string }{action: "", ref: "v4.1.1"}, want: "", wantErr: true, }, { name: "actions/checkout with empty tag returns error", args: struct{ action, ref string }{action: "actions/checkout", ref: ""}, want: "", wantErr: true, }, { name: "actions/setup-node with v1 is an array", args: struct{ action, ref string }{action: "actions/setup-node", ref: "v1"}, want: "f1f314fca9dfce2769ece7d933488f076716723e", wantErr: false, }, { name: "anchore/sbom-action/download-syft with a sub-action works", args: struct{ action, ref string }{action: "anchore/sbom-action/download-syft", ref: "v0.14.3"}, want: "78fc58e266e87a38d4194b2137a3d4e9bcaf7ca1", wantErr: false, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() got, err := GetChecksum(ctx, config.GHActions{}, ghcli, tt.args.action, tt.args.ref) if tt.wantErr { require.Error(t, err, "Wanted error, got none") require.Empty(t, got, "Wanted empty string, got %v", got) return } require.NoError(t, err, "Wanted no error, got %v", err) require.Equal(t, tt.want, got, "Wanted %v, got %v", tt.want, got) }) } } 07070100000030000041ED00000000000000000000000266CD8D3200000000000000000000000000000000000000000000002100000000frizbee-0.1.2/pkg/replacer/image07070100000031000081A400000000000000000000000166CD8D3200002043000000000000000000000000000000000000002A00000000frizbee-0.1.2/pkg/replacer/image/image.go// // Copyright 2024 Stacklok, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package image provides utilities to work with container images. package image import ( "context" "errors" "fmt" "slices" "strings" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" dockerparser "github.com/moby/buildkit/frontend/dockerfile/parser" "github.com/stacklok/frizbee/internal/cli" "github.com/stacklok/frizbee/pkg/interfaces" "github.com/stacklok/frizbee/pkg/utils/config" "github.com/stacklok/frizbee/pkg/utils/store" ) const ( // ContainerImageRegex is regular expression pattern to match container image usage in YAML // nolint:lll ContainerImageRegex = `image\s*:\s*["']?([^\s"']+/[^\s"']+|[^\s"']+)(:[^\s"']+)?(@[^\s"']+)?["']?|FROM\s+(--platform=[^\s]+[^\s]*\s+)?([^\s]+(/[^\s]+)?(:[^\s]+)?(@[^\s]+)?)` prefixFROM = "FROM " prefixImage = "image: " // ReferenceType is the type of the reference ReferenceType = "container" ) // Parser is a struct to replace container image references with digests type Parser struct { regex string cache store.RefCacher } type unresolvedImage struct { imageRef string flags []string } // New creates a new Parser func New() *Parser { return &Parser{ regex: ContainerImageRegex, cache: store.NewRefCacher(), } } // SetCache sets the cache to store the image references func (p *Parser) SetCache(cache store.RefCacher) { p.cache = cache } // SetRegex sets the regular expression pattern to match container image usage func (p *Parser) SetRegex(regex string) { p.regex = regex } // GetRegex returns the regular expression pattern to match container image usage func (p *Parser) GetRegex() string { return p.regex } // Replace replaces the container image reference with the digest func (p *Parser) Replace( ctx context.Context, matchedLine string, _ interfaces.REST, cfg config.Config, ) (*interfaces.EntityRef, error) { var imageRef string var extraArgs string // Trim the prefix hasFROMPrefix := false hasImagePrefix := false // Check if the image reference has the FROM prefix, i.e. Dockerfile if strings.HasPrefix(matchedLine, prefixFROM) { parsedFrom, err := getRefFromDockerfileFROM(matchedLine) if err != nil { return nil, err } // Check if the image reference should be excluded, i.e. scratch if shouldSkipImageRef(&cfg, parsedFrom.imageRef) { return nil, fmt.Errorf("image reference %s should be excluded - %w", matchedLine, interfaces.ErrReferenceSkipped) } imageRef = parsedFrom.imageRef extraArgs = strings.Join(parsedFrom.flags, " ") if extraArgs != "" { extraArgs += " " } hasFROMPrefix = true } else if strings.HasPrefix(matchedLine, prefixImage) { // Check if the image reference has the image prefix, i.e. Kubernetes or Docker Compose YAML imageRef = strings.TrimPrefix(matchedLine, prefixImage) // Check if the image reference should be excluded, i.e. scratch if shouldSkipImageRef(&cfg, imageRef) { return nil, fmt.Errorf("image reference %s should be excluded - %w", matchedLine, interfaces.ErrReferenceSkipped) } hasImagePrefix = true } else { imageRef = matchedLine } // Get the digest of the image reference imageRefWithDigest, err := GetImageDigestFromRef(ctx, imageRef, cfg.Platform, p.cache) if err != nil { return nil, err } // Add the prefix back if hasFROMPrefix { imageRefWithDigest.Prefix = fmt.Sprintf("%s%s%s", prefixFROM, extraArgs, imageRefWithDigest.Prefix) } else if hasImagePrefix { imageRefWithDigest.Prefix = fmt.Sprintf("%s%s", prefixImage, imageRefWithDigest.Prefix) } // Return the reference return imageRefWithDigest, nil } // ConvertToEntityRef converts a container image reference to an EntityRef func (_ *Parser) ConvertToEntityRef(reference string) (*interfaces.EntityRef, error) { reference = strings.TrimPrefix(reference, prefixImage) reference = strings.TrimPrefix(reference, prefixFROM) var sep string var frags []string if strings.Contains(reference, "@") { sep = "@" } else if strings.Contains(reference, ":") { sep = ":" } if sep != "" { frags = strings.Split(reference, sep) if len(frags) != 2 { return nil, fmt.Errorf("invalid container reference: %s", reference) } } else { frags = []string{reference, "latest"} } return &interfaces.EntityRef{ Name: frags[0], Ref: frags[1], Type: ReferenceType, }, nil } // GetImageDigestFromRef returns the digest of a container image reference // from a name.Reference. func GetImageDigestFromRef(ctx context.Context, imageRef, platform string, cache store.RefCacher) (*interfaces.EntityRef, error) { // Parse the image reference ref, err := name.ParseReference(imageRef) if err != nil { return nil, err } opts := []remote.Option{ remote.WithContext(ctx), remote.WithUserAgent(cli.UserAgent), remote.WithAuthFromKeychain(authn.DefaultKeychain), } // Set the platform if provided if platform != "" { platformSplit := strings.Split(platform, "/") if len(platformSplit) != 2 { return nil, errors.New("platform must be in the format os/arch") } opts = append(opts, remote.WithPlatform(v1.Platform{ OS: platformSplit[0], Architecture: platformSplit[1], })) } // Get the digest of the image reference var digest string if cache != nil { if d, ok := cache.Load(imageRef); ok { digest = d } else { desc, err := remote.Get(ref, opts...) if err != nil { return nil, err } digest = desc.Digest.String() cache.Store(imageRef, digest) } } else { desc, err := remote.Get(ref, opts...) if err != nil { return nil, err } digest = desc.Digest.String() } // Compare the digest with the reference and return the original reference if they already match if digest == ref.Identifier() { return nil, fmt.Errorf("image already referenced by digest: %s %w", imageRef, interfaces.ErrReferenceSkipped) } return &interfaces.EntityRef{ Name: ref.Context().Name(), Ref: digest, Type: ReferenceType, Tag: ref.Identifier(), }, nil } func shouldSkipImageRef(cfg *config.Config, ref string) bool { // Parse the image reference nameRef, err := name.ParseReference(ref) if err != nil { // we wouldn't know how to resolve this reference, so let's skip return true } imageName := getImageNameFromRef(nameRef) if slices.Contains(cfg.Images.ImageFilter.ExcludeImages, imageName) { return true } tag := nameRef.Identifier() return slices.Contains(cfg.Images.ImageFilter.ExcludeTags, tag) } // TODO(jakub): this is a bit of a hack, but I didn't find a better way to get just the name func getImageNameFromRef(nameRef name.Reference) string { fullRepositoryName := nameRef.Context().Name() parts := strings.Split(fullRepositoryName, "/") if len(parts) > 1 { return parts[len(parts)-1] } return "" } func getRefFromDockerfileFROM(line string) (unresolvedImage, error) { parseResult, err := dockerparser.Parse(strings.NewReader(line)) if err != nil { return unresolvedImage{}, fmt.Errorf("failed to parse Dockerfile line: %w", err) } if len(parseResult.AST.Children) == 0 || parseResult.AST.Children[0] == nil || strings.ToUpper(parseResult.AST.Children[0].Value) != "FROM" { return unresolvedImage{}, errors.New("invalid Dockerfile line: the first parsed node is not FROM") } fromNode := parseResult.AST.Children[0] imgNode := parseResult.AST.Children[0].Next if imgNode == nil { return unresolvedImage{}, errors.New("invalid Dockerfile line: no image node found") } return unresolvedImage{ imageRef: imgNode.Value, flags: fromNode.Flags, }, nil } 07070100000032000081A400000000000000000000000166CD8D3200001811000000000000000000000000000000000000002F00000000frizbee-0.1.2/pkg/replacer/image/image_test.gopackage image import ( "context" "testing" "github.com/stretchr/testify/require" "github.com/stacklok/frizbee/pkg/interfaces" "github.com/stacklok/frizbee/pkg/utils/config" "github.com/stacklok/frizbee/pkg/utils/store" ) func TestNewParser(t *testing.T) { t.Parallel() tests := []struct { name string }{ {"New parser initialization"}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() parser := New() require.NotNil(t, parser, "Parser should not be nil") require.Equal(t, ContainerImageRegex, parser.regex, "Default regex should be ContainerImageRegex") require.NotNil(t, parser.cache, "Cache should be initialized") }) } } func TestSetCache(t *testing.T) { t.Parallel() tests := []struct { name string cache store.RefCacher }{ {"Set cache for parser", store.NewRefCacher()}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() parser := New() parser.SetCache(tt.cache) require.Equal(t, tt.cache, parser.cache, "Cache should be set correctly") }) } } func TestSetAndGetRegex(t *testing.T) { t.Parallel() tests := []struct { name string newRegex string }{ {"Set and get new regex", `new-regex`}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() parser := New() parser.SetRegex(tt.newRegex) require.Equal(t, tt.newRegex, parser.GetRegex(), "Regex should be set and retrieved correctly") }) } } func TestReplaceExcludedPath(t *testing.T) { t.Parallel() parser := New() ctx := context.Background() cfg := config.Config{ Images: config.Images{ ImageFilter: config.ImageFilter{ ExcludeImages: []string{"scratch"}, ExcludeTags: []string{"latest"}, }, }, } tests := []struct { name string matchedLine string expected error }{ { "Do not replace scratch FROM image", "FROM scratch", interfaces.ErrReferenceSkipped, }, { "Do not replace ubuntu:latest", "FROM ubuntu:latest", interfaces.ErrReferenceSkipped, }, { "Do not replace ubuntu:latest with AS", "FROM ubuntu:latest AS builder", interfaces.ErrReferenceSkipped, }, { "Do not replace ubuntu without a tag", "FROM ubuntu", interfaces.ErrReferenceSkipped, }, { "Do not replace ubuntu without a tag with a stage", "FROM ubuntu AS builder", interfaces.ErrReferenceSkipped, }, { "Replace ubuntu:22.04", "FROM ubuntu:22.04", nil, }, { "Replace ubuntu:22.04 with AS", "FROM ubuntu:22.04 AS builder", nil, }, { "Replace ubuntu:22.04 with AS", "FROM --platform=linux/amd64 ubuntu:22.04 AS builder", nil, }, { "Replace with repo reference and tag", "FROM ghcr.io/stacklok/minder/helm/minder:0.20231123.829_ref.26ca90b", nil, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() _, err := parser.Replace(ctx, tt.matchedLine, nil, cfg) if tt.expected == nil { require.NoError(t, err, "Should not return error for excluded path") } else { require.Error(t, err, "Should return error for excluded path") require.ErrorIs(t, err, tt.expected, "Unexpected error") } }) } } func TestConvertToEntityRef(t *testing.T) { t.Parallel() parser := New() tests := []struct { name string reference string wantErr bool }{ {"Valid container reference with tag", "ghcr.io/stacklok/minder/helm/minder:0.20231123.829_ref.26ca90b", false}, {"Valid container reference with digest", "ghcr.io/stacklok/minder/helm/minder@sha256:a29f8a8d28f0af7f70a4b3dd3e33c8c8cc5cf9e88e802e2700cf272a0b6140ec", false}, {"Invalid reference format", "invalid:reference:format", true}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() ref, err := parser.ConvertToEntityRef(tt.reference) if tt.wantErr { require.Error(t, err, "Expected error but got none") } else { require.NoError(t, err, "Expected no error but got %v", err) require.NotNil(t, ref, "EntityRef should not be nil") } }) } } func TestGetImageDigestFromRef(t *testing.T) { t.Parallel() ctx := context.Background() tests := []struct { name string refstr string want string wantErr bool }{ { name: "Valid image reference 1", refstr: "ghcr.io/stacklok/minder/helm/minder:0.20231123.829_ref.26ca90b", want: "sha256:a29f8a8d28f0af7f70a4b3dd3e33c8c8cc5cf9e88e802e2700cf272a0b6140ec", }, { name: "Valid image reference 2", refstr: "devopsfaith/krakend:2.5.0", want: "sha256:6a3c8e5e1a4948042bfb364ed6471e16b4a26d0afb6c3c01ebcb88b3fa551036", }, { name: "Invalid ref string", refstr: "ghcr.io/stacklok/minder/helm/minder!", wantErr: true, }, { name: "Nonexistent container in nonexistent registry", refstr: "beeeeer.io/ipa/toppling-goliath/king-sue:1.0.0", wantErr: true, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() got, err := GetImageDigestFromRef(ctx, tt.refstr, "", nil) if tt.wantErr { require.Error(t, err) require.Nil(t, got) return } require.NoError(t, err) require.Equal(t, tt.want, got.Ref) }) } } func TestShouldSkipImage(t *testing.T) { t.Parallel() tests := []struct { name string ref string skip bool }{ // skip cases {"Skip scratch", "scratch", true}, {"Skip ubuntu without a tag", "ubuntu", true}, {"Skip ubuntu:latest", "ubuntu:latest", true}, // keep cases {"Do not skip ubuntu:22.04", "ubuntu:22.04", false}, {"Do not skip with repo reference and tag", "myrepo/myimage:1.2.3", false}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() config := &config.Config{ Images: config.Images{ ImageFilter: config.ImageFilter{ ExcludeImages: []string{"scratch"}, ExcludeTags: []string{"latest"}, }, }, } got := shouldSkipImageRef(config, tt.ref) require.Equal(t, tt.skip, got, "shouldSkipImageRef should return the correct exclusion status") }) } } 07070100000033000081A400000000000000000000000166CD8D3200002846000000000000000000000000000000000000002700000000frizbee-0.1.2/pkg/replacer/replacer.go// // Copyright 2024 Stacklok, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package replacer provide common replacer implementation package replacer import ( "bufio" "context" "fmt" "io" "path/filepath" "regexp" "sort" "strings" "sync" mapset "github.com/deckarep/golang-set/v2" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/osfs" "golang.org/x/sync/errgroup" "github.com/stacklok/frizbee/internal/traverse" "github.com/stacklok/frizbee/pkg/interfaces" "github.com/stacklok/frizbee/pkg/replacer/actions" "github.com/stacklok/frizbee/pkg/replacer/image" "github.com/stacklok/frizbee/pkg/utils/config" "github.com/stacklok/frizbee/pkg/utils/ghrest" ) // ReplaceResult holds a slice of all processed files along with a map of their modified content type ReplaceResult struct { Processed []string Modified map[string]string } // ListResult holds the result of the list methods type ListResult struct { Processed []string Entities []interfaces.EntityRef } // Replacer is an object with methods to replace references with digests type Replacer struct { parser interfaces.Parser rest interfaces.REST cfg config.Config } // NewGitHubActionsReplacer creates a new replacer for GitHub actions func NewGitHubActionsReplacer(cfg *config.Config) *Replacer { cfg = config.MergeUserConfig(cfg) return &Replacer{ cfg: *cfg, parser: actions.New(), rest: ghrest.NewClient(""), } } // NewContainerImagesReplacer creates a new replacer for container images func NewContainerImagesReplacer(cfg *config.Config) *Replacer { cfg = config.MergeUserConfig(cfg) return &Replacer{ cfg: *cfg, parser: image.New(), rest: ghrest.NewClient(""), } } // WithGitHubClientFromToken creates an authenticated GitHub client from a token func (r *Replacer) WithGitHubClientFromToken(token string) *Replacer { client := ghrest.NewClient(token) r.rest = client return r } // WithGitHubClient sets the GitHub client to use func (r *Replacer) WithGitHubClient(client interfaces.REST) *Replacer { r.rest = client return r } // WithUserRegex sets a user-provided regex for the parser func (r *Replacer) WithUserRegex(regex string) *Replacer { if r.parser != nil && regex != "" { r.parser.SetRegex(regex) } return r } // WithCacheDisabled disables caching func (r *Replacer) WithCacheDisabled() *Replacer { r.parser.SetCache(nil) return r } // ParseString parses and returns the referenced entity pinned by its digest func (r *Replacer) ParseString(ctx context.Context, entityRef string) (*interfaces.EntityRef, error) { return r.parser.Replace(ctx, entityRef, r.rest, r.cfg) } // ParsePath parses and replaces all entity references in the provided directory func (r *Replacer) ParsePath(ctx context.Context, dir string) (*ReplaceResult, error) { return parsePathInFS(ctx, r.parser, r.rest, r.cfg, osfs.New(filepath.Dir(dir), osfs.WithBoundOS()), filepath.Base(dir)) } // ParsePathInFS parses and replaces all entity references in the provided file system func (r *Replacer) ParsePathInFS(ctx context.Context, bfs billy.Filesystem, base string) (*ReplaceResult, error) { return parsePathInFS(ctx, r.parser, r.rest, r.cfg, bfs, base) } // ParseFile parses and replaces all entity references in the provided file func (r *Replacer) ParseFile(ctx context.Context, f io.Reader) (bool, string, error) { return parseAndReplaceReferencesInFile(ctx, f, r.parser, r.rest, r.cfg) } // ListPath lists all entity references in the provided directory func (r *Replacer) ListPath(dir string) (*ListResult, error) { return listReferencesInFS(r.parser, osfs.New(filepath.Dir(dir), osfs.WithBoundOS()), filepath.Base(dir)) } // ListPathInFS lists all entity references in the provided file system func (r *Replacer) ListPathInFS(bfs billy.Filesystem, base string) (*ListResult, error) { return listReferencesInFS(r.parser, bfs, base) } // ListInFile lists all entities in the provided file func (r *Replacer) ListInFile(f io.Reader) (*ListResult, error) { found, err := listReferencesInFile(f, r.parser) if err != nil { return nil, err } res := &ListResult{} res.Entities = found.ToSlice() // Sort the slice sort.Slice(res.Entities, func(i, j int) bool { return res.Entities[i].Name < res.Entities[j].Name }) // All good return res, nil } func parsePathInFS( ctx context.Context, parser interfaces.Parser, rest interfaces.REST, cfg config.Config, bfs billy.Filesystem, base string, ) (*ReplaceResult, error) { var eg errgroup.Group var mu sync.Mutex res := ReplaceResult{ Processed: make([]string, 0), Modified: make(map[string]string), } // Traverse all YAML/YML files in dir err := traverse.YamlDockerfiles(bfs, base, func(path string) error { eg.Go(func() error { file, err := bfs.Open(path) if err != nil { return fmt.Errorf("failed to open file %s: %w", path, err) } // nolint:errcheck // ignore error defer file.Close() // Parse the content of the file and update the matching references modified, updatedFile, err := parseAndReplaceReferencesInFile(ctx, file, parser, rest, cfg) if err != nil { return fmt.Errorf("failed to modify references in %s: %w", path, err) } mu.Lock() // Store the file name to the processed batch res.Processed = append(res.Processed, path) // Store the updated file content if it was modified if modified { res.Modified[path] = updatedFile } mu.Unlock() // All good return nil }) return nil }) if err != nil { return nil, err } if err := eg.Wait(); err != nil { return nil, err } // All good return &res, nil } func listReferencesInFS(parser interfaces.Parser, bfs billy.Filesystem, base string) (*ListResult, error) { var eg errgroup.Group var mu sync.Mutex res := ListResult{ Processed: make([]string, 0), Entities: make([]interfaces.EntityRef, 0), } found := mapset.NewSet[interfaces.EntityRef]() // Traverse all related files err := traverse.YamlDockerfiles(bfs, base, func(path string) error { eg.Go(func() error { file, err := bfs.Open(path) if err != nil { return fmt.Errorf("failed to open file %s: %w", path, err) } defer file.Close() // nolint:errcheck // Parse the content of the file and list the matching references foundRefs, err := listReferencesInFile(file, parser) if err != nil { return fmt.Errorf("failed to list references in %s: %w", path, err) } // Store the file name to the processed batch mu.Lock() res.Processed = append(res.Processed, path) found = found.Union(foundRefs) mu.Unlock() // All good return nil }) return nil }) if err != nil { return nil, err } if err := eg.Wait(); err != nil { return nil, err } res.Entities = found.ToSlice() // Sort the slice sort.Slice(res.Entities, func(i, j int) bool { return res.Entities[i].Name < res.Entities[j].Name }) // All good return &res, nil } func parseAndReplaceReferencesInFile( ctx context.Context, f io.Reader, parser interfaces.Parser, rest interfaces.REST, cfg config.Config, ) (bool, string, error) { var contentBuilder strings.Builder var ret *interfaces.EntityRef modified := false // Compile the regular expression re, err := regexp.Compile(parser.GetRegex()) if err != nil { return false, "", err } // Read the file line by line scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() // Skip commented lines if strings.HasPrefix(strings.TrimLeft(line, " \t\n\r"), "#") { // Write the line to the content builder buffer contentBuilder.WriteString(line + "\n") continue } // See if we can match an entity reference in the line newLine := re.ReplaceAllStringFunc(line, func(matchedLine string) string { // Modify the reference in the line ret, err = parser.Replace(ctx, matchedLine, rest, cfg) if err != nil { // Return the original line as we don't want to update it in case something errored out return matchedLine } // Construct the new line, comments in dockerfiles are handled differently than yml files if strings.Contains(matchedLine, "FROM") { return fmt.Sprintf("%s%s:%s@%s", ret.Prefix, ret.Name, ret.Tag, ret.Ref) } return fmt.Sprintf("%s%s@%s # %s", ret.Prefix, ret.Name, ret.Ref, ret.Tag) }) // Check if the line was modified and set the modified flag to true if it was if newLine != line { modified = true } // Write the line to the content builder buffer contentBuilder.WriteString(newLine + "\n") } // Check for errors during the scan if err := scanner.Err(); err != nil { return false, "", err } // Return the workflow content return modified, contentBuilder.String(), nil } // listReferencesInFile takes the given file reader and returns a map of all references, action or images it finds func listReferencesInFile( f io.Reader, parser interfaces.Parser, ) (mapset.Set[interfaces.EntityRef], error) { found := mapset.NewSet[interfaces.EntityRef]() // Compile the regular expression re, err := regexp.Compile(parser.GetRegex()) if err != nil { return nil, err } // Read the file line by line scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() // Skip commented lines if strings.HasPrefix(strings.TrimLeft(line, " \t\n\r"), "#") { continue } // See if we can match an entity reference in the line foundEntries := re.FindAllString(line, -1) // nolint:gosimple if foundEntries != nil { for _, entry := range foundEntries { e, err := parser.ConvertToEntityRef(entry) if err != nil { continue } found.Add(*e) } } } // Check for errors during the scan if err := scanner.Err(); err != nil { return nil, err } // Return the found references return found, nil } 07070100000034000081A400000000000000000000000166CD8D32000089CF000000000000000000000000000000000000002C00000000frizbee-0.1.2/pkg/replacer/replacer_test.go// // Copyright 2024 Stacklok, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package replacer import ( "context" "os" "strings" "testing" "github.com/go-git/go-billy/v5/memfs" "github.com/stretchr/testify/require" "github.com/stacklok/frizbee/internal/cli" "github.com/stacklok/frizbee/pkg/interfaces" "github.com/stacklok/frizbee/pkg/replacer/actions" "github.com/stacklok/frizbee/pkg/replacer/image" "github.com/stacklok/frizbee/pkg/utils/config" "github.com/stacklok/frizbee/pkg/utils/ghrest" ) func TestReplacer_ParseContainerImageString(t *testing.T) { t.Parallel() type args struct { refstr string } tests := []struct { name string args args want *interfaces.EntityRef wantErr bool }{ { name: "dockerfile - tag", args: args{ refstr: "FROM golang:1.22.2", }, want: &interfaces.EntityRef{ Name: "index.docker.io/library/golang", Ref: "sha256:d5302d40dc5fbbf38ec472d1848a9d2391a13f93293a6a5b0b87c99dc0eaa6ae", Type: image.ReferenceType, Tag: "1.22.2", Prefix: "FROM ", }, wantErr: false, }, { name: "dockerfile - tag, stage and platform", args: args{ refstr: "FROM --platform=linux/s390x golang:1.22.2 AS build", }, want: &interfaces.EntityRef{ Name: "index.docker.io/library/golang", Ref: "sha256:d5302d40dc5fbbf38ec472d1848a9d2391a13f93293a6a5b0b87c99dc0eaa6ae", Type: image.ReferenceType, Tag: "1.22.2", Prefix: "FROM --platform=linux/s390x ", }, wantErr: false, }, { name: "dockerfile - no tag", args: args{ refstr: "FROM golang", }, want: nil, wantErr: true, }, { name: "dockerfile - latest", args: args{ refstr: "FROM golang:latest", }, want: nil, wantErr: true, }, { name: "dockerfile - already by digest", args: args{ refstr: "FROM golang:1.22.2@sha256:aca60c1f21de99aa3a34e653f0cdc8c8ea8fe6480359229809d5bcb974f599ec", }, want: nil, wantErr: true, }, { name: "dockerfile - scratch", args: args{ refstr: "FROM scratch", }, want: nil, wantErr: true, }, { name: "valid 1", args: args{ refstr: "ghcr.io/stacklok/minder/helm/minder:0.20231123.829_ref.26ca90b", }, want: &interfaces.EntityRef{ Name: "ghcr.io/stacklok/minder/helm/minder", Ref: "sha256:a29f8a8d28f0af7f70a4b3dd3e33c8c8cc5cf9e88e802e2700cf272a0b6140ec", Type: image.ReferenceType, Tag: "0.20231123.829_ref.26ca90b", Prefix: "", }, wantErr: false, }, { name: "valid 2", args: args{ refstr: "devopsfaith/krakend:2.5.0", }, want: &interfaces.EntityRef{ Name: "index.docker.io/devopsfaith/krakend", Ref: "sha256:6a3c8e5e1a4948042bfb364ed6471e16b4a26d0afb6c3c01ebcb88b3fa551036", Type: image.ReferenceType, Tag: "2.5.0", Prefix: "", }, wantErr: false, }, { name: "image with no tag is skipped", args: args{ refstr: "image: nginx", }, want: nil, wantErr: true, }, { name: "image with latest tag is skipped", args: args{ refstr: "image: nginx:latest", }, want: nil, wantErr: true, }, { name: "invalid ref string", args: args{ refstr: "ghcr.io/stacklok/minder/helm/minder!", }, want: nil, wantErr: true, }, { name: "nonexistent container in nonexistent registry", args: args{ refstr: "beeeeer.io/ipa/toppling-goliath/king-sue:1.0.0", }, want: nil, wantErr: true, }, // TODO: Create a dedicated container image for this test and push it so that latest doesnt change //{ // name: "container reference with no tag or digest", // args: args{ // refstr: "nginx", // }, // want: &interfaces.EntityRef{ // Name: "index.docker.io/library/nginx", // Ref: "sha256:faef0b115e699b1e70b1f9a939ea2bc62c26485f6b72e91c8a7b236f1f8589c1", // Type: image.ReferenceType, // Tag: "latest", // Prefix: "", // }, // wantErr: false, //}, { name: "invalid reference with special characters", args: args{ refstr: "nginx@#$$%%^&*", }, want: nil, wantErr: true, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() ctx := context.Background() config := &config.Config{ Images: config.Images{ ImageFilter: config.ImageFilter{ ExcludeTags: []string{"latest"}, }, }, } r := NewContainerImagesReplacer(config) got, err := r.ParseString(ctx, tt.args.refstr) if tt.wantErr { require.Error(t, err) require.Empty(t, got) return } require.NoError(t, err) require.Equal(t, tt.want, got) }) } } func TestReplacer_ParseGitHubActionString(t *testing.T) { t.Parallel() type args struct { action string } tests := []struct { name string args args want *interfaces.EntityRef wantErr bool }{ { name: "action using a container via docker://avtodev/markdown-lint:v1", args: args{ action: "uses: docker://avtodev/markdown-lint:v1", }, want: &interfaces.EntityRef{ Name: "index.docker.io/avtodev/markdown-lint", Ref: "sha256:6aeedc2f49138ce7a1cd0adffc1b1c0321b841dc2102408967d9301c031949ee", Type: image.ReferenceType, Tag: "v1", Prefix: "uses: docker://", }, wantErr: false, }, { name: "actions/checkout with v4.1.1", args: args{ action: "actions/checkout@v4.1.1", }, want: &interfaces.EntityRef{ Name: "actions/checkout", Ref: "b4ffde65f46336ab88eb53be808477a3936bae11", Type: actions.ReferenceType, Tag: "v4.1.1", Prefix: "", }, wantErr: false, }, { name: "actions/checkout with v3.6.0", args: args{ action: "uses: actions/checkout@v3.6.0", }, want: &interfaces.EntityRef{ Name: "actions/checkout", Ref: "f43a0e5ff2bd294095638e18286ca9a3d1956744", Type: actions.ReferenceType, Tag: "v3.6.0", Prefix: "uses: ", }, wantErr: false, }, { name: "actions/checkout with checksum returns checksum", args: args{ action: "actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f", }, want: nil, wantErr: true, }, { name: "aquasecurity/trivy-action with 0.14.0", args: args{ action: "aquasecurity/trivy-action@0.14.0", }, want: &interfaces.EntityRef{ Name: "aquasecurity/trivy-action", Ref: "2b6a709cf9c4025c5438138008beaddbb02086f0", Type: actions.ReferenceType, Tag: "0.14.0", Prefix: "", }, wantErr: false, }, { name: "aquasecurity/trivy-action with branch returns checksum", args: args{ action: "aquasecurity/trivy-action@bump-trivy", }, want: &interfaces.EntityRef{ Name: "aquasecurity/trivy-action", Ref: "fb5e1b36be448e92ca98648c661bd7e9da1f1317", Type: actions.ReferenceType, Tag: "bump-trivy", Prefix: "", }, wantErr: false, }, { name: "aquasecurity/trivy-action with ignored branch returns error", args: args{ action: "aquasecurity/trivy-action@main", }, wantErr: true, }, { name: "actions/checkout with invalid tag returns error", args: args{ action: "actions/checkout@v4.1.1.1", }, want: nil, wantErr: true, }, { name: "actions/checkout with invalid action returns error", args: args{ action: "invalid-action@v4.1.1", }, want: nil, wantErr: true, }, { name: "actions/checkout with empty action returns error", args: args{ action: "@v4.1.1", }, want: nil, wantErr: true, }, { name: "actions/checkout with empty tag returns error", args: args{ action: "actions/checkout", }, want: nil, wantErr: true, }, { name: "actions/setup-node with v1 is an array", args: args{ action: "actions/setup-node@v1", }, want: &interfaces.EntityRef{ Name: "actions/setup-node", Ref: "f1f314fca9dfce2769ece7d933488f076716723e", Type: actions.ReferenceType, Tag: "v1", Prefix: "", }, }, { name: "anchore/sbom-action/download-syft with a sub-action works", args: args{ action: "anchore/sbom-action/download-syft@v0.14.3", }, want: &interfaces.EntityRef{ Name: "anchore/sbom-action/download-syft", Ref: "78fc58e266e87a38d4194b2137a3d4e9bcaf7ca1", Type: actions.ReferenceType, Tag: "v0.14.3", Prefix: "", }, }, { name: "invalid action reference", args: args{ action: "invalid-reference", }, want: nil, wantErr: true, }, { name: "missing action tag", args: args{ action: "actions/checkout", }, want: nil, wantErr: true, }, { name: "action with special characters", args: args{ action: "actions/checkout@#$$%%^&*", }, want: nil, wantErr: true, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() ctx := context.Background() conf := &config.Config{ GHActions: config.GHActions{ Filter: config.Filter{ ExcludeBranches: []string{"main"}, }, }, Images: config.Images{ ImageFilter: config.ImageFilter{ ExcludeTags: []string{"latest"}, }, }, } r := NewGitHubActionsReplacer(conf).WithGitHubClientFromToken(os.Getenv("GITHUB_TOKEN")) got, err := r.ParseString(ctx, tt.args.action) if tt.wantErr { require.Error(t, err) require.Empty(t, got) return } require.NoError(t, err) require.Equal(t, tt.want, got) }) } } func TestReplacer_ParseContainerImagesInFile(t *testing.T) { t.Parallel() testCases := []struct { name string before string expected string modified bool wantErr bool }{ { name: "Replace image reference", before: ` version: v1 services: - name: kube-apiserver image: registry.k8s.io/kube-apiserver:v1.20.0 - name: kube-controller-manager image: registry.k8s.io/kube-controller-manager:v1.15.0 - name: minder-app image: minder:latest `, expected: ` version: v1 services: - name: kube-apiserver image: registry.k8s.io/kube-apiserver@sha256:8b8125d7a6e4225b08f04f65ca947b27d0cc86380bf09fab890cc80408230114 # v1.20.0 - name: kube-controller-manager image: registry.k8s.io/kube-controller-manager@sha256:835f32a5cdb30e86f35675dd91f9c7df01d48359ab8b51c1df866a2c7ea2e870 # v1.15.0 - name: minder-app image: minder:latest `, modified: true, }, { name: "No image reference modification", before: ` version: v1 services: - name: minder-app image: minder:latest `, expected: ` version: v1 services: - name: minder-app image: minder:latest `, modified: false, }, { name: "Invalid image reference format", before: ` version: v1 services: - name: invalid-service image: invalid@@reference `, expected: ` version: v1 services: - name: invalid-service image: invalid@@reference `, modified: false, wantErr: false, }, { name: "Multiple valid image references with one commented", before: ` version: v1 services: - name: kube-apiserver image: registry.k8s.io/kube-apiserver:v1.20.0 - name: kube-controller-manager image: registry.k8s.io/kube-controller-manager:v1.15.0 - name: minder-app image: minder:latest # - name: nginx # image: nginx:latest `, expected: ` version: v1 services: - name: kube-apiserver image: registry.k8s.io/kube-apiserver@sha256:8b8125d7a6e4225b08f04f65ca947b27d0cc86380bf09fab890cc80408230114 # v1.20.0 - name: kube-controller-manager image: registry.k8s.io/kube-controller-manager@sha256:835f32a5cdb30e86f35675dd91f9c7df01d48359ab8b51c1df866a2c7ea2e870 # v1.15.0 - name: minder-app image: minder:latest # - name: nginx # image: nginx:latest `, modified: true, }, { name: "Valid image reference without specifying the tag", before: ` apiVersion: v1 kind: Pod metadata: name: mount-host namespace: playground spec: containers: - name: mount-host image: alpine command: ["sleep"] args: ["infinity"] volumeMounts: - name: host-root mountPath: /host readOnly: true volumes: - name: host-root hostPath: path: / type: Directory `, modified: false, }, { name: "A complex dockerfile", before: ` ARG BASE_IMAGE=alpine FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.2.1@sha256:8879a398dedf0aadaacfbd332b29ff2f84bc39ae6d4e9c0a1109db27ac5ba012 AS xx FROM --platform=$BUILDPLATFORM golang:1.20.4-alpine3.16 AS builder COPY --from=xx / / RUN apk add --update alpine-sdk ca-certificates openssl clang lld ARG TARGETPLATFORM RUN xx-apk --update add musl-dev gcc # lld has issues building static binaries for ppc so prefer ld for it RUN [ "$(xx-info arch)" != "ppc64le" ] || XX_CC_PREFER_LINKER=ld xx-clang --setup-target-triple RUN xx-go --wrap WORKDIR /usr/local/src/dex ARG GOPROXY ENV CGO_ENABLED=1 COPY go.mod go.sum ./ COPY api/v2/go.mod api/v2/go.sum ./api/v2/ RUN go mod download COPY . . RUN make release-binary RUN xx-verify /go/bin/dex && xx-verify /go/bin/docker-entrypoint FROM alpine:3.18.2 AS stager RUN mkdir -p /var/dex RUN mkdir -p /etc/dex COPY config.docker.yaml /etc/dex/ FROM alpine:3.18.2 AS gomplate ARG TARGETOS ARG TARGETARCH ARG TARGETVARIANT ENV GOMPLATE_VERSION=v3.11.4 RUN wget -O /usr/local/bin/gomplate \ "https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${TARGETOS:-linux}-${TARGETARCH:-amd64}${TARGETVARIANT}" \ && chmod +x /usr/local/bin/gomplate # For Dependabot to detect base image versions FROM alpine:3.18.2 AS alpine FROM gcr.io/distroless/static:latest AS distroless FROM $BASE_IMAGE # Dex connectors, such as GitHub and Google logins require root certificates. # Proper installations should manage those certificates, but it's a bad user # experience when this doesn't work out of the box. # # See https://go.dev/src/crypto/x509/root_linux.go for Go root CA bundle locations. COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=stager --chown=1001:1001 /var/dex /var/dex COPY --from=stager --chown=1001:1001 /etc/dex /etc/dex # Copy module files for CVE scanning / dependency analysis. COPY --from=builder /usr/local/src/dex/go.mod /usr/local/src/dex/go.sum /usr/local/src/dex/ COPY --from=builder /usr/local/src/dex/api/v2/go.mod /usr/local/src/dex/api/v2/go.sum /usr/local/src/dex/api/v2/ COPY --from=builder /go/bin/dex /usr/local/bin/dex COPY --from=builder /go/bin/docker-entrypoint /usr/local/bin/docker-entrypoint COPY --from=builder /usr/local/src/dex/web /srv/dex/web COPY --from=gomplate /usr/local/bin/gomplate /usr/local/bin/gomplate USER 1001:1001 ENTRYPOINT ["/usr/local/bin/docker-entrypoint"] CMD ["dex", "serve", "/etc/dex/config.docker.yaml"] `, expected: ` ARG BASE_IMAGE=alpine FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.2.1@sha256:8879a398dedf0aadaacfbd332b29ff2f84bc39ae6d4e9c0a1109db27ac5ba012 AS xx FROM --platform=$BUILDPLATFORM index.docker.io/library/golang:1.20.4-alpine3.16@sha256:6469405d7297f82d56195c90a3270b0806ef4bd897aa0628477d9959ab97a577 AS builder COPY --from=xx / / RUN apk add --update alpine-sdk ca-certificates openssl clang lld ARG TARGETPLATFORM RUN xx-apk --update add musl-dev gcc # lld has issues building static binaries for ppc so prefer ld for it RUN [ "$(xx-info arch)" != "ppc64le" ] || XX_CC_PREFER_LINKER=ld xx-clang --setup-target-triple RUN xx-go --wrap WORKDIR /usr/local/src/dex ARG GOPROXY ENV CGO_ENABLED=1 COPY go.mod go.sum ./ COPY api/v2/go.mod api/v2/go.sum ./api/v2/ RUN go mod download COPY . . RUN make release-binary RUN xx-verify /go/bin/dex && xx-verify /go/bin/docker-entrypoint FROM index.docker.io/library/alpine:3.18.2@sha256:82d1e9d7ed48a7523bdebc18cf6290bdb97b82302a8a9c27d4fe885949ea94d1 AS stager RUN mkdir -p /var/dex RUN mkdir -p /etc/dex COPY config.docker.yaml /etc/dex/ FROM index.docker.io/library/alpine:3.18.2@sha256:82d1e9d7ed48a7523bdebc18cf6290bdb97b82302a8a9c27d4fe885949ea94d1 AS gomplate ARG TARGETOS ARG TARGETARCH ARG TARGETVARIANT ENV GOMPLATE_VERSION=v3.11.4 RUN wget -O /usr/local/bin/gomplate \ "https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${TARGETOS:-linux}-${TARGETARCH:-amd64}${TARGETVARIANT}" \ && chmod +x /usr/local/bin/gomplate # For Dependabot to detect base image versions FROM index.docker.io/library/alpine:3.18.2@sha256:82d1e9d7ed48a7523bdebc18cf6290bdb97b82302a8a9c27d4fe885949ea94d1 AS alpine FROM gcr.io/distroless/static:latest AS distroless FROM $BASE_IMAGE # Dex connectors, such as GitHub and Google logins require root certificates. # Proper installations should manage those certificates, but it's a bad user # experience when this doesn't work out of the box. # # See https://go.dev/src/crypto/x509/root_linux.go for Go root CA bundle locations. COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=stager --chown=1001:1001 /var/dex /var/dex COPY --from=stager --chown=1001:1001 /etc/dex /etc/dex # Copy module files for CVE scanning / dependency analysis. COPY --from=builder /usr/local/src/dex/go.mod /usr/local/src/dex/go.sum /usr/local/src/dex/ COPY --from=builder /usr/local/src/dex/api/v2/go.mod /usr/local/src/dex/api/v2/go.sum /usr/local/src/dex/api/v2/ COPY --from=builder /go/bin/dex /usr/local/bin/dex COPY --from=builder /go/bin/docker-entrypoint /usr/local/bin/docker-entrypoint COPY --from=builder /usr/local/src/dex/web /srv/dex/web COPY --from=gomplate /usr/local/bin/gomplate /usr/local/bin/gomplate USER 1001:1001 ENTRYPOINT ["/usr/local/bin/docker-entrypoint"] CMD ["dex", "serve", "/etc/dex/config.docker.yaml"] `, modified: true, }, } for _, tt := range testCases { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() ctx := context.Background() r := NewContainerImagesReplacer(&config.Config{ Images: config.Images{ ImageFilter: config.ImageFilter{ ExcludeTags: []string{"latest"}, }, }, }) modified, newContent, err := r.ParseFile(ctx, strings.NewReader(tt.before)) if tt.wantErr { require.False(t, modified) require.Equal(t, tt.before, newContent) require.Error(t, err) return } require.NoError(t, err) if tt.modified { require.True(t, modified) if tt.expected != "" { require.Equal(t, tt.expected, newContent) } else { require.NotEmpty(t, tt.before, newContent) } } else { require.False(t, modified) require.Equal(t, tt.before, newContent) } }) } } func TestReplacer_ParseGitHubActionsInFile(t *testing.T) { t.Parallel() testCases := []struct { name string before string expected string regex string modified bool wantErr bool useCustomRegex bool }{ { name: "Replace image reference", before: ` name: Linter on: pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: ./minder/server.yml # this should not be replaced - uses: actions/checkout@v2 - uses: xt0rted/markdownlint-problem-matcher@v1 - name: "Run Markdown linter" uses: docker://avtodev/markdown-lint:v1 with: args: src/*.md `, expected: ` name: Linter on: pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: ./minder/server.yml # this should not be replaced - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 - uses: xt0rted/markdownlint-problem-matcher@c17ca40d1376f60aba7e7d38a8674a3f22f7f5b0 # v1 - name: "Run Markdown linter" uses: docker://index.docker.io/avtodev/markdown-lint@sha256:6aeedc2f49138ce7a1cd0adffc1b1c0321b841dc2102408967d9301c031949ee # v1 with: args: src/*.md `, modified: true, wantErr: false, }, { name: "Replace actions with tags, not with branches", before: ` name: Linter on: pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: ./minder/server.yml # this should not be replaced - uses: actions/checkout@v2 - uses: aquasecurity/trivy-action@main - name: "Run Markdown linter" uses: docker://avtodev/markdown-lint:v1 with: args: src/*.md `, expected: ` name: Linter on: pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: ./minder/server.yml # this should not be replaced - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 - uses: aquasecurity/trivy-action@main - name: "Run Markdown linter" uses: docker://index.docker.io/avtodev/markdown-lint@sha256:6aeedc2f49138ce7a1cd0adffc1b1c0321b841dc2102408967d9301c031949ee # v1 with: args: src/*.md `, modified: true, wantErr: false, }, { name: "No action reference modification", before: ` name: Linter on: pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: ./minder/server.yml # this should not be replaced # - uses: actions/checkout@v2 `, expected: ` name: Linter on: pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: ./minder/server.yml # this should not be replaced # - uses: actions/checkout@v2 `, modified: false, }, { name: "Invalid action reference format", before: ` name: Linter on: pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: invalid@@reference `, expected: ` name: Linter on: pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: invalid@@reference `, modified: false, wantErr: false, }, { name: "Multiple valid action references", before: ` name: Linter on: pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: ./minder/server.yml # this should not be replaced - uses: actions/checkout@v2 - uses: xt0rted/markdownlint-problem-matcher@v1 `, expected: ` name: Linter on: pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: ./minder/server.yml # this should not be replaced - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 - uses: xt0rted/markdownlint-problem-matcher@c17ca40d1376f60aba7e7d38a8674a3f22f7f5b0 # v1 `, modified: true, }, { name: "Fail with custom regex", before: ` name: Linter on: pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: ./minder/server.yml # this should not be replaced - uses: actions/checkout@v2 - uses: xt0rted/markdownlint-problem-matcher@v1 - name: "Run Markdown linter" uses: docker://avtodev/markdown-lint:v1 with: args: src/*.md `, expected: ` name: Linter on: pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: ./minder/server.yml # this should not be replaced - uses: actions/checkout@v2 - uses: xt0rted/markdownlint-problem-matcher@v1 - name: "Run Markdown linter" uses: docker://avtodev/markdown-lint:v1 with: args: src/*.md `, modified: false, wantErr: false, regex: "invalid-regexp", useCustomRegex: true, }, } for _, tt := range testCases { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() ctx := context.Background() r := NewGitHubActionsReplacer(&config.Config{ GHActions: config.GHActions{ Filter: config.Filter{ ExcludeBranches: []string{"*"}, }, }, }).WithGitHubClientFromToken(os.Getenv(cli.GitHubTokenEnvKey)) if tt.useCustomRegex { r = r.WithUserRegex(tt.regex) } modified, newContent, err := r.ParseFile(ctx, strings.NewReader(tt.before)) if tt.modified { require.True(t, modified) require.Equal(t, tt.expected, newContent) } else { require.False(t, modified) require.Equal(t, tt.before, newContent) } if tt.wantErr { require.False(t, modified) require.Equal(t, tt.before, newContent) require.Error(t, err) return } require.NoError(t, err) require.Equal(t, tt.expected, newContent) }) } } func TestReplacer_NewGitHubActionsReplacer(t *testing.T) { t.Parallel() cfg := &config.Config{} tests := []struct { name string cfg *config.Config }{ {name: "valid config", cfg: cfg}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() r := NewGitHubActionsReplacer(tt.cfg) require.NotNil(t, r) require.IsType(t, &Replacer{}, r) require.IsType(t, actions.New(), r.parser) }) } } func TestReplacer_NewContainerImagesReplacer(t *testing.T) { t.Parallel() cfg := &config.Config{} tests := []struct { name string cfg *config.Config }{ {name: "valid config", cfg: cfg}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() r := NewContainerImagesReplacer(tt.cfg) require.NotNil(t, r) require.IsType(t, &Replacer{}, r) require.IsType(t, image.New(), r.parser) }) } } func TestReplacer_WithGitHubClient(t *testing.T) { t.Parallel() r := &Replacer{} tests := []struct { name string token string }{ {name: "valid token", token: "valid_token"}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() r = r.WithGitHubClientFromToken(tt.token) require.NotNil(t, r) require.IsType(t, ghrest.NewClient(tt.token), r.rest) }) } } func TestReplacer_WithUserRegex(t *testing.T) { t.Parallel() r := &Replacer{parser: actions.New()} tests := []struct { name string regex string }{ {name: "valid regex", regex: `^test-regex$`}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() r = r.WithUserRegex(tt.regex) require.Equal(t, tt.regex, r.parser.GetRegex()) }) } } func TestReplacer_WithCacheDisabled(t *testing.T) { t.Parallel() r := &Replacer{parser: actions.New()} tests := []struct { name string }{ {name: "disable cache"}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() r = r.WithCacheDisabled() // we don't test if this passed here because it's an internal implementation detail // but let's ensure we don't panic for some reason }) } } func TestReplacer_ParsePathInFS(t *testing.T) { t.Parallel() r := &Replacer{parser: actions.New(), cfg: config.Config{}} fs := memfs.New() tests := []struct { name string base string wantErr bool }{ {name: "valid base", base: "some-base", wantErr: false}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() _, err := r.ParsePathInFS(context.Background(), fs, tt.base) if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) } }) } } func TestReplacer_ListPathInFS(t *testing.T) { t.Parallel() r := &Replacer{parser: actions.New(), cfg: config.Config{}} fs := memfs.New() tests := []struct { name string base string wantErr bool }{ {name: "valid base", base: "some-base", wantErr: false}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() _, err := r.ListPathInFS(fs, tt.base) if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) } }) } } func TestReplacer_ListContainerImagesInFile(t *testing.T) { t.Parallel() testCases := []struct { name string before string expected *ListResult regex string wantErr bool useCustomRegex bool }{ { name: "Lust image reference", before: ` version: v1 services: - name: kube-apiserver image: registry.k8s.io/kube-apiserver:v1.20.0 - name: kube-controller-manager image: registry.k8s.io/kube-controller-manager:v1.15.0 - name: minder-app image: minder:latest `, expected: &ListResult{ Entities: []interfaces.EntityRef{ { Name: "registry.k8s.io/kube-apiserver", Ref: "v1.20.0", Type: image.ReferenceType, }, { Name: "registry.k8s.io/kube-controller-manager", Ref: "v1.15.0", Type: image.ReferenceType, }, { Name: "minder", Ref: "latest", Type: image.ReferenceType, }, }, }, wantErr: false, }, { name: "No image reference modification", before: ` version: v1 services: - name: minder-app # image: minder:latest `, expected: &ListResult{ Entities: []interfaces.EntityRef{}, }, wantErr: false, }, { name: "Invalid image reference format", before: ` version: v1 services: - name: invalid-service image: invalid@@reference `, expected: &ListResult{ Entities: []interfaces.EntityRef{}, }, wantErr: false, }, { name: "Multiple valid image references with one commented", before: ` version: v1 services: - name: kube-apiserver image: registry.k8s.io/kube-apiserver@sha256:8b8125d7a6e4225b08f04f65ca947b27d0cc86380bf09fab890cc80408230114 # v1.20.0 - name: kube-controller-manager image: registry.k8s.io/kube-controller-manager@sha256:835f32a5cdb30e86f35675dd91f9c7df01d48359ab8b51c1df866a2c7ea2e870 # v1.15.0 - name: minder-app image: minder:latest # - name: nginx # image: nginx:latest `, expected: &ListResult{ Entities: []interfaces.EntityRef{ { Name: "registry.k8s.io/kube-apiserver", Ref: "sha256:8b8125d7a6e4225b08f04f65ca947b27d0cc86380bf09fab890cc80408230114", Type: image.ReferenceType, }, { Name: "registry.k8s.io/kube-controller-manager", Ref: "sha256:835f32a5cdb30e86f35675dd91f9c7df01d48359ab8b51c1df866a2c7ea2e870", Type: image.ReferenceType, }, { Name: "minder", Ref: "latest", Type: image.ReferenceType, }, }, }, }, { name: "Valid image reference without specifying the tag", before: ` apiVersion: v1 kind: Pod metadata: name: mount-host namespace: playground spec: containers: - name: mount-host image: alpine command: ["sleep"] args: ["infinity"] volumeMounts: - name: host-root mountPath: /host readOnly: true volumes: - name: host-root hostPath: path: / type: Directory `, expected: &ListResult{ Entities: []interfaces.EntityRef{ { Name: "alpine", Ref: "latest", Type: image.ReferenceType, }, }, }, }, } for _, tt := range testCases { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() r := NewContainerImagesReplacer(&config.Config{}) listRes, err := r.ListInFile(strings.NewReader(tt.before)) if tt.wantErr { require.Nil(t, listRes) require.Error(t, err) return } require.NoError(t, err) require.Equal(t, len(tt.expected.Entities), len(listRes.Entities)) for _, entity := range tt.expected.Entities { require.Contains(t, listRes.Entities, entity) } }) } } func TestReplacer_ListGitHubActionsInFile(t *testing.T) { t.Parallel() testCases := []struct { name string before string expected *ListResult regex string wantErr bool useCustomRegex bool }{ { name: "List image reference", before: ` name: Linter on: pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: ./minder/server.yml # this should not be listed - uses: actions/checkout@v2 - uses: xt0rted/markdownlint-problem-matcher@v1 - name: "Run Markdown linter" uses: docker://avtodev/markdown-lint:v1 with: args: src/*.md `, expected: &ListResult{ Entities: []interfaces.EntityRef{ { Name: "actions/checkout", Ref: "v2", Type: actions.ReferenceType, }, { Name: "xt0rted/markdownlint-problem-matcher", Ref: "v1", Type: actions.ReferenceType, }, { Name: "avtodev/markdown-lint", Ref: "v1", Type: image.ReferenceType, }, }, }, wantErr: false, }, { name: "No action references", before: ` name: Linter on: pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: ./minder/server.yml # this should not be replaced # - uses: actions/checkout@v2 `, expected: &ListResult{ Entities: []interfaces.EntityRef{}, }, wantErr: false, }, { name: "Invalid action reference format", before: ` name: Linter on: pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: invalid@@reference `, expected: &ListResult{ Entities: []interfaces.EntityRef{}, }, wantErr: false, }, { name: "Multiple valid action references", before: ` name: Linter on: pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: ./minder/server.yml # this should not be replaced - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 - uses: xt0rted/markdownlint-problem-matcher@c17ca40d1376f60aba7e7d38a8674a3f22f7f5b0 # v1 `, expected: &ListResult{ Entities: []interfaces.EntityRef{ { Name: "actions/checkout", Ref: "ee0669bd1cc54295c223e0bb666b733df41de1c5", Type: actions.ReferenceType, }, { Name: "xt0rted/markdownlint-problem-matcher", Ref: "c17ca40d1376f60aba7e7d38a8674a3f22f7f5b0", Type: actions.ReferenceType, }, }, }, }, { name: "Fail with custom regex", before: ` name: Linter on: pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: ./minder/server.yml # this should not be replaced - uses: actions/checkout@v2 - uses: xt0rted/markdownlint-problem-matcher@v1 - name: "Run Markdown linter" uses: docker://avtodev/markdown-lint:v1 with: args: src/*.md `, expected: &ListResult{ Entities: []interfaces.EntityRef{}, }, wantErr: false, regex: "invalid-regexp", useCustomRegex: true, }, } for _, tt := range testCases { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() r := NewGitHubActionsReplacer(&config.Config{}).WithGitHubClientFromToken(os.Getenv(cli.GitHubTokenEnvKey)) if tt.useCustomRegex { r = r.WithUserRegex(tt.regex) } listRes, err := r.ListInFile(strings.NewReader(tt.before)) if tt.wantErr { require.Nil(t, listRes) require.Error(t, err) return } require.NoError(t, err) require.Equal(t, len(tt.expected.Entities), len(listRes.Entities)) for _, entity := range tt.expected.Entities { require.Contains(t, listRes.Entities, entity) } }) } } 07070100000035000041ED00000000000000000000000266CD8D3200000000000000000000000000000000000000000000001800000000frizbee-0.1.2/pkg/utils07070100000036000041ED00000000000000000000000266CD8D3200000000000000000000000000000000000000000000001F00000000frizbee-0.1.2/pkg/utils/config07070100000037000081A400000000000000000000000166CD8D320000114D000000000000000000000000000000000000002900000000frizbee-0.1.2/pkg/utils/config/config.go// // Copyright 2024 Stacklok, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package config provides the frizbee configuration. package config import ( "errors" "fmt" "io" "os" "path/filepath" "slices" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/osfs" "github.com/spf13/cobra" "gopkg.in/yaml.v3" ) type contextConfigKey struct{} // ContextConfigKey is the context key for the configuration. // nolint:gochecknoglobals // this is a context key var ContextConfigKey = contextConfigKey{} var ( // ErrNoConfigInContext is returned when no configuration is found in the context. ErrNoConfigInContext = errors.New("no configuration found in context") ) // FromCommand returns the configuration from the cobra command. func FromCommand(cmd *cobra.Command) (*Config, error) { ctx := cmd.Context() cfg, ok := ctx.Value(ContextConfigKey).(*Config) if !ok { return nil, ErrNoConfigInContext } // If the platform flag is set, override the platform in the configuration. if cmd.Flags().Lookup("platform") != nil { cfg.Platform = cmd.Flag("platform").Value.String() } return cfg, nil } // Config is the frizbee configuration. type Config struct { Platform string `yaml:"platform" mapstructure:"platform"` GHActions GHActions `yaml:"ghactions" mapstructure:"ghactions"` Images Images `yaml:"images" mapstructure:"images"` } // GHActions is the GitHub Actions configuration. type GHActions struct { Filter `yaml:",inline" mapstructure:",inline"` } // Filter is a common configuration for filtering out patterns. type Filter struct { // Exclude is a list of patterns to exclude. Exclude []string `yaml:"exclude" mapstructure:"exclude"` ExcludeBranches []string `yaml:"exclude_branches" mapstructure:"exclude_branches"` } // Images is the image configuration. type Images struct { ImageFilter `yaml:",inline" mapstructure:",inline"` } // ImageFilter is the image filter configuration. type ImageFilter struct { // ExcludeImages is a regex that must match in order for an image to be excluded and not pinned ExcludeImages []string `yaml:"exclude_images" mapstructure:"exclude_images"` ExcludeTags []string `yaml:"exclude_tags" mapstructure:"exclude_tags"` } // ParseConfigFile parses a configuration file. func ParseConfigFile(configfile string) (*Config, error) { bfs := osfs.New(".") return ParseConfigFileFromFS(bfs, configfile) } // DefaultConfig returns the default configuration. func DefaultConfig() *Config { return &Config{ GHActions: GHActions{ Filter: Filter{ ExcludeBranches: []string{"main", "master"}, }, }, Images: Images{ ImageFilter: ImageFilter{ ExcludeImages: []string{"scratch"}, ExcludeTags: []string{"latest"}, }, }, } } // MergeUserConfig merges the user configuration with the default configuration. // mostly making sure that we don't try to pin the scratch image func MergeUserConfig(userConfig *Config) *Config { if userConfig == nil { return DefaultConfig() } if userConfig.Images.ExcludeImages == nil { userConfig.Images.ExcludeImages = []string{"scratch"} } if !slices.Contains(userConfig.Images.ExcludeImages, "scratch") { userConfig.Images.ExcludeImages = append(userConfig.Images.ExcludeImages, "scratch") } return userConfig } // ParseConfigFileFromFS parses a configuration file from a filesystem. func ParseConfigFileFromFS(fs billy.Filesystem, configfile string) (*Config, error) { cfg := DefaultConfig() cleancfgfile := filepath.Clean(configfile) cfgF, err := fs.Open(cleancfgfile) if err != nil { if os.IsNotExist(err) { return cfg, nil } return nil, fmt.Errorf("failed to open config file: %w", err) } defer cfgF.Close() // nolint:errcheck dec := yaml.NewDecoder(cfgF) if err := dec.Decode(cfg); err != nil { if err != io.EOF { return nil, fmt.Errorf("failed to decode config file: %w", err) } } return cfg, nil } 07070100000038000081A400000000000000000000000166CD8D32000010F5000000000000000000000000000000000000002E00000000frizbee-0.1.2/pkg/utils/config/config_test.gopackage config import ( "context" "testing" "github.com/go-git/go-billy/v5/memfs" "github.com/spf13/cobra" "github.com/stretchr/testify/require" ) func TestFromCommand(t *testing.T) { t.Parallel() testCases := []struct { name string contextCfg *Config platformFlag string expectedCfg *Config expectError bool }{ { name: "NoConfigInContext", contextCfg: nil, expectError: true, }, { name: "WithConfigInContext", contextCfg: &Config{Platform: "linux/arm64"}, expectedCfg: &Config{Platform: "linux/arm64"}, }, { name: "WithPlatformFlag", contextCfg: &Config{Platform: "linux/amd64"}, platformFlag: "windows/arm64", expectedCfg: &Config{Platform: "windows/arm64"}, }, } for _, tt := range testCases { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() ctx := context.Background() cmd := &cobra.Command{} if tt.contextCfg != nil { ctx := context.WithValue(ctx, ContextConfigKey, tt.contextCfg) cmd.SetContext(ctx) } else { cmd.SetContext(ctx) } if tt.platformFlag != "" { cmd.Flags().String("platform", "", "platform") require.NoError(t, cmd.Flags().Set("platform", tt.platformFlag)) } cfg, err := FromCommand(cmd) if tt.expectError { require.Error(t, err) require.Nil(t, cfg) } else { require.NoError(t, err) require.Equal(t, tt.expectedCfg, cfg) } }) } } func TestParseConfigFile(t *testing.T) { t.Parallel() testCases := []struct { name string fsContent map[string]string fileName string expectedResult *Config expectError bool }{ { name: "FileNotFound", fileName: "nonexistent.yaml", expectedResult: DefaultConfig(), }, { name: "InvalidYaml", fileName: "invalid.yaml", fsContent: map[string]string{"invalid.yaml": "invalid yaml content"}, expectError: true, }, { name: "DontIngoreBranches", fileName: "dont_ignore_branches.yaml", fsContent: map[string]string{ "dont_ignore_branches.yaml": ` platform: linux/amd64 ghactions: exclude_branches: `, }, expectedResult: &Config{ Platform: "linux/amd64", GHActions: GHActions{ Filter: Filter{ ExcludeBranches: []string{}, }, }, Images: Images{ ImageFilter: ImageFilter{ ExcludeImages: []string{"scratch"}, ExcludeTags: []string{"latest"}, }, }, }, }, { name: "ValidYaml", fileName: "valid.yaml", fsContent: map[string]string{ "valid.yaml": ` platform: linux/amd64 ghactions: exclude: - pattern1 - pattern2 images: exclude_images: - notthisone exclude_tags: - notthistag `, }, expectedResult: &Config{ Platform: "linux/amd64", GHActions: GHActions{ Filter: Filter{ Exclude: []string{"pattern1", "pattern2"}, ExcludeBranches: []string{"main", "master"}, }, }, Images: Images{ ImageFilter: ImageFilter{ ExcludeImages: []string{"notthisone"}, ExcludeTags: []string{"notthistag"}, }, }, }, }, { name: "EmptyFile", fileName: "empty.yaml", fsContent: map[string]string{"empty.yaml": ""}, expectedResult: DefaultConfig(), }, } for _, tt := range testCases { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() fs := memfs.New() for name, content := range tt.fsContent { f, _ := fs.Create(name) _, _ = f.Write([]byte(content)) require.NoError(t, f.Close()) } cfg, err := ParseConfigFileFromFS(fs, tt.fileName) if tt.expectError { require.Error(t, err) } else { require.NoError(t, err) require.Equal(t, tt.expectedResult.Platform, cfg.Platform) if cfg.GHActions.Exclude != nil { require.Equal(t, tt.expectedResult.GHActions.Exclude, cfg.GHActions.Exclude) } if cfg.Images.ExcludeImages != nil { require.Equal(t, tt.expectedResult.Images.ExcludeImages, cfg.Images.ExcludeImages) } if cfg.Images.ExcludeTags != nil { require.Equal(t, tt.expectedResult.Images.ExcludeTags, cfg.Images.ExcludeTags) } if cfg.GHActions.ExcludeBranches != nil { require.Equal(t, tt.expectedResult.GHActions.ExcludeBranches, cfg.GHActions.ExcludeBranches) } } }) } } 07070100000039000041ED00000000000000000000000266CD8D3200000000000000000000000000000000000000000000001F00000000frizbee-0.1.2/pkg/utils/ghrest0707010000003A000081A400000000000000000000000166CD8D3200000844000000000000000000000000000000000000002900000000frizbee-0.1.2/pkg/utils/ghrest/ghrest.go// // Copyright 2024 Stacklok, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package ghrest provides a wrapper around the go-github client that implements the internal REST API package ghrest import ( "bytes" "context" "io" "net/http" "github.com/google/go-github/v61/github" ) // Client is the struct that contains the GitHub REST API client // this struct implements the REST API type Client struct { client *github.Client } // NewClient creates a new instance of GhRest func NewClient(token string) *Client { ghcli := github.NewClient(nil) if token != "" { ghcli = ghcli.WithAuthToken(token) } return &Client{ client: ghcli, } } // NewRequest creates an API request. A relative URL can be provided in urlStr, // which will be resolved to the BaseURL of the Client. Relative URLS should // always be specified without a preceding slash. If specified, the value // pointed to by body is JSON encoded and included as the request body. func (c *Client) NewRequest(method, requestUrl string, body any) (*http.Request, error) { return c.client.NewRequest(method, requestUrl, body) } // Do sends an API request and returns the API response. func (c *Client) Do(ctx context.Context, req *http.Request) (*http.Response, error) { var buf bytes.Buffer // The GitHub client closes the response body, so we need to capture it // in a buffer so that we can return it to the caller resp, err := c.client.Do(ctx, req, &buf) if err != nil && resp == nil { return nil, err } if resp.Response != nil { resp.Response.Body = io.NopCloser(&buf) } return resp.Response, err } 0707010000003B000081A400000000000000000000000166CD8D3200000AD3000000000000000000000000000000000000002E00000000frizbee-0.1.2/pkg/utils/ghrest/ghrest_test.gopackage ghrest import ( "context" "errors" "io" "net/http" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/h2non/gock.v1" ) // nolint:gocyclo func TestClientFunctions(t *testing.T) { t.Parallel() testCases := []struct { name string token string method string url string mockResponse *gock.Response expectedMethod string expectedURL string expectError bool expectedStatus int expectedBody string }{ { name: "NewClient", token: "test_token", expectedMethod: "", expectedURL: "", }, { name: "NewRequest GET", token: "", method: "GET", url: "test_url", expectedMethod: http.MethodGet, expectedURL: "https://api.github.com/test_url", }, { name: "Do successful request", token: "", method: "GET", url: "test", mockResponse: gock.New("https://api.github.com").Get("/test").Reply(200).BodyString(`{"message": "hello world"}`), expectedMethod: http.MethodGet, expectedURL: "https://api.github.com/test", expectedStatus: http.StatusOK, expectedBody: `{"message": "hello world"}`, }, { name: "Do failed request", token: "", method: "GET", url: "test", mockResponse: gock.New("https://api.github.com").Get("/test").ReplyError(errors.New("failed request")), expectedMethod: http.MethodGet, expectedURL: "https://api.github.com/test", expectError: true, }, } for _, tt := range testCases { tt := tt if tt.mockResponse != nil { defer gock.Off() //gock.DisableNetworking() //t.Logf("Mock response configured for %s %s", tt.method, tt.url) } client := NewClient(tt.token) if tt.name == "NewClient" { assert.NotNil(t, client, "NewClient returned nil") assert.NotNil(t, client.client, "NewClient returned client with nil GitHub client") return } req, err := client.NewRequest(tt.method, tt.url, nil) require.NoError(t, err) require.Equal(t, req.Method, tt.expectedMethod) require.Equal(t, req.URL.String(), tt.expectedURL) if tt.name == "NewRequest GET" { return } ctx := context.Background() resp, err := client.Do(ctx, req) if tt.expectError { require.NotNil(t, err, "Expected error, got nil") require.Nil(t, resp, "Expected nil response, got %v", resp) return } require.Nil(t, err, "Expected no error, got %v", err) require.Equal(t, resp.StatusCode, tt.expectedStatus) body, err := io.ReadAll(resp.Body) require.Nil(t, err) require.Equal(t, string(body), tt.expectedBody) defer resp.Body.Close() // nolint:errcheck } } 0707010000003C000041ED00000000000000000000000266CD8D3200000000000000000000000000000000000000000000001E00000000frizbee-0.1.2/pkg/utils/store0707010000003D000081A400000000000000000000000166CD8D3200000702000000000000000000000000000000000000002700000000frizbee-0.1.2/pkg/utils/store/cache.go// // Copyright 2024 Stacklok, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package store provides utilities to work with a cache store. package store import ( "github.com/puzpuzpuz/xsync" ) // RefCacher is an interface for caching references. type RefCacher interface { Store(key, value string) Load(key string) (string, bool) } type refCacher struct { cache *xsync.MapOf[string, string] } // NewRefCacher returns a new RefCacher. The default implementation is // thread-safe. func NewRefCacher() RefCacher { return &refCacher{ cache: xsync.NewMapOf[string](), } } // Store stores a key-value pair. func (r *refCacher) Store(key, value string) { r.cache.Store(key, value) } // Load loads a value for a given key. func (r *refCacher) Load(key string) (string, bool) { return r.cache.Load(key) } type unsafeCacher struct { cache map[string]string } // NewUnsafeCacher returns a new RefCacher that's not thread-safe. func NewUnsafeCacher() RefCacher { return &unsafeCacher{ cache: map[string]string{}, } } // Store stores a key-value pair. func (r *unsafeCacher) Store(key, value string) { r.cache[key] = value } // Load loads a value for a given key. func (r *unsafeCacher) Load(key string) (string, bool) { v, ok := r.cache[key] return v, ok } 0707010000003E000081A400000000000000000000000166CD8D32000009BE000000000000000000000000000000000000002C00000000frizbee-0.1.2/pkg/utils/store/cache_test.gopackage store import ( "fmt" "testing" "github.com/stretchr/testify/require" ) type Cacher interface { Store(key, value string) Load(key string) (string, bool) } // TestCacher tests the creation and basic functionality of both refCacher and unsafeCacher. func TestCacher(t *testing.T) { t.Parallel() testCases := []struct { name string cacher Cacher key string storeValue string loadKey string expectedVal string expectFound bool }{ { name: "RefCacher store and load existing key", cacher: NewRefCacher(), key: "key1", storeValue: "value1", loadKey: "key1", expectedVal: "value1", expectFound: true, }, { name: "RefCacher load non-existing key", cacher: NewRefCacher(), key: "key1", storeValue: "value1", loadKey: "key2", expectedVal: "", expectFound: false, }, { name: "UnsafeCacher store and load existing key", cacher: NewUnsafeCacher(), key: "key1", storeValue: "value1", loadKey: "key1", expectedVal: "value1", expectFound: true, }, { name: "UnsafeCacher load non-existing key", cacher: NewUnsafeCacher(), key: "key1", storeValue: "value1", loadKey: "key2", expectedVal: "", expectFound: false, }, } for _, tt := range testCases { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() tt.cacher.Store(tt.key, tt.storeValue) val, ok := tt.cacher.Load(tt.loadKey) require.Equal(t, tt.expectFound, ok) require.Equal(t, tt.expectedVal, val) }) } } // TestConcurrency tests the thread-safety of refCacher. func TestConcurrency(t *testing.T) { t.Parallel() cacher := NewRefCacher() iterations := 1000 done := make(chan bool) // Concurrently store values for i := 0; i < iterations; i++ { go func(i int) { key := fmt.Sprintf("key%d", i) value := fmt.Sprintf("value%d", i) cacher.Store(key, value) done <- true }(i) } // Wait for all goroutines to finish storing for i := 0; i < iterations; i++ { <-done } // Concurrently load values for i := 0; i < iterations; i++ { go func(i int) { key := fmt.Sprintf("key%d", i) val, ok := cacher.Load(key) expectedVal := fmt.Sprintf("value%d", i) require.True(t, ok) require.Equal(t, expectedVal, val) done <- true }(i) } // Wait for all goroutines to finish loading for i := 0; i < iterations; i++ { <-done } } 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!404 blocks
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor