File frizbee-0.1.7.obscpio of Package frizbee
07070100000000000081A400000000000000000000000167E26F59000000EF000000000000000000000000000000000000001B00000000frizbee-0.1.7/.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
07070100000001000081A400000000000000000000000167E26F59000009C3000000000000000000000000000000000000001C00000000frizbee-0.1.7/.golangci.ymlrun:
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
- copyloopvar
- decorder
- dogsled
- dupl
- errcheck
- errname
- exhaustive
- 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
07070100000002000081A400000000000000000000000167E26F5900000B80000000000000000000000000000000000000001F00000000frizbee-0.1.7/.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 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'
07070100000003000081A400000000000000000000000167E26F5900000C93000000000000000000000000000000000000002100000000frizbee-0.1.7/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/
07070100000004000081A400000000000000000000000167E26F5900000EA0000000000000000000000000000000000000001E00000000frizbee-0.1.7/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/).
07070100000005000081A400000000000000000000000167E26F5900002C50000000000000000000000000000000000000001600000000frizbee-0.1.7/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.
07070100000006000081A400000000000000000000000167E26F59000019FC000000000000000000000000000000000000001800000000frizbee-0.1.7/README.md
---
[](https://coveralls.io/github/stacklok/frizbee?branch=main) | [](https://opensource.org/licenses/Apache-2.0) | [](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 install github.com/stacklok/frizbee@v0.1.2
# add the path to your .bashrc
export PATH=$PATH:$(go env GOPATH)/bin
# 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 source 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).
07070100000007000081A400000000000000000000000167E26F5900000333000000000000000000000000000000000000001A00000000frizbee-0.1.7/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.
07070100000008000081A400000000000000000000000167E26F59000001F7000000000000000000000000000000000000001B00000000frizbee-0.1.7/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.yml07070100000009000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001200000000frizbee-0.1.7/cmd0707010000000A000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001A00000000frizbee-0.1.7/cmd/actions0707010000000B000081A400000000000000000000000167E26F5900000C2B000000000000000000000000000000000000002500000000frizbee-0.1.7/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
}
if err := cliFlags.CheckModified(res.Modified); 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
}
0707010000000C000081A400000000000000000000000167E26F5900000AAE000000000000000000000000000000000000002200000000frizbee-0.1.7/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)
}
}
0707010000000D000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001800000000frizbee-0.1.7/cmd/image0707010000000E000081A400000000000000000000000167E26F5900000AD4000000000000000000000000000000000000002100000000frizbee-0.1.7/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
}
0707010000000F000081A400000000000000000000000167E26F59000009F9000000000000000000000000000000000000002000000000frizbee-0.1.7/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)
}
}
07070100000010000081A400000000000000000000000167E26F59000007D4000000000000000000000000000000000000001A00000000frizbee-0.1.7/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)
}
07070100000011000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001A00000000frizbee-0.1.7/cmd/version07070100000012000081A400000000000000000000000167E26F5900000440000000000000000000000000000000000000002500000000frizbee-0.1.7/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)
},
}
}
07070100000013000081A400000000000000000000000167E26F5900000724000000000000000000000000000000000000001500000000frizbee-0.1.7/go.modmodule github.com/stacklok/frizbee
go 1.23.2
toolchain go1.24.1
require (
github.com/deckarep/golang-set/v2 v2.8.0
github.com/go-git/go-billy/v5 v5.6.2
github.com/google/go-containerregistry v0.20.3
github.com/google/go-github/v66 v66.0.0
github.com/moby/buildkit v0.20.2
github.com/olekukonko/tablewriter v0.0.5
github.com/puzpuzpuz/xsync v1.5.2
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
golang.org/x/sync v0.12.0
gopkg.in/h2non/gock.v1 v1.1.2
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v27.5.1+incompatible // indirect
github.com/docker/distribution v2.8.3+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.11 // 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/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/vbatts/tar-split v0.11.6 // indirect
golang.org/x/sys v0.29.0 // indirect
google.golang.org/protobuf v1.36.3 // indirect
)
07070100000014000081A400000000000000000000000167E26F5900002FEC000000000000000000000000000000000000001500000000frizbee-0.1.7/go.sumgithub.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
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.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/docker/cli v27.5.1+incompatible h1:JB9cieUT9YNiMITtIsguaN55PLOHhBSz3LKVc6cqWaY=
github.com/docker/cli v27.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+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.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/go-cmp v0.5.2/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.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
github.com/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M=
github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4=
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.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
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.20.2 h1:qIeR47eQ1tzI1rwz0on3Xx2enRw/1CKjFhoONVcTlMA=
github.com/moby/buildkit v0.20.2/go.mod h1:DhaF82FjwOElTftl0JUAJpH/SUIUx4UvcFncLeOtlDI=
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.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
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/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
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.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs=
github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI=
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/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
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.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
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.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
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=
07070100000015000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001700000000frizbee-0.1.7/internal07070100000016000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001B00000000frizbee-0.1.7/internal/cli07070100000017000081A400000000000000000000000167E26F59000018B7000000000000000000000000000000000000002200000000frizbee-0.1.7/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 (
"errors"
"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
}
}
// CheckModified checks if any files were modified and returns an error if there were.
func (r *Helper) CheckModified(modified map[string]string) error {
if len(modified) > 0 && r.ErrOnModified {
if !r.Quiet {
for path := range modified {
r.Logf("Modified: %s\n", path)
}
}
return errors.New("files were modified")
}
return nil
}
// 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
}
07070100000018000081A400000000000000000000000167E26F5900001B8B000000000000000000000000000000000000002700000000frizbee-0.1.7/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 {
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 TestCheckModified(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
helper *Helper
modified map[string]string
expectOutput string
expectError bool
}{
{
name: "NoFilesModified",
helper: &Helper{
ErrOnModified: true,
Quiet: false,
Cmd: &cobra.Command{},
},
modified: map[string]string{},
expectOutput: "",
expectError: false,
},
{
name: "FilesModifiedWithErrorFlag",
helper: &Helper{
ErrOnModified: true,
Quiet: false,
Cmd: &cobra.Command{},
},
modified: map[string]string{
"file1.txt": "modified content",
"file2.txt": "another modified content",
},
expectOutput: "Modified: file1.txt\nModified: file2.txt",
expectError: true,
},
{
name: "FilesModifiedWithoutErrorFlag",
helper: &Helper{
ErrOnModified: false,
Quiet: false,
Cmd: &cobra.Command{},
},
modified: map[string]string{
"file1.txt": "modified content",
},
expectOutput: "",
expectError: false,
},
{
name: "FilesModifiedWithErrorFlagAndQuiet",
helper: &Helper{
ErrOnModified: true,
Quiet: true,
Cmd: &cobra.Command{},
},
modified: map[string]string{
"file1.txt": "modified content",
},
expectOutput: "",
expectError: true,
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Set up command error output
var output strings.Builder
tt.helper.Cmd.SetErr(&output)
// Call the CheckModified method
err := tt.helper.CheckModified(tt.modified)
// Check if error status matches expectation
if tt.expectError {
assert.Error(t, err)
assert.Equal(t, "files were modified", err.Error())
} else {
assert.NoError(t, err)
}
// Check if output contains expected strings
if tt.expectOutput != "" {
// We can't guarantee the order of map iteration, so we check that
// the output contains each expected line
for path := range tt.modified {
if !tt.helper.Quiet {
assert.Contains(t, output.String(), "Modified: "+path)
}
}
}
})
}
}
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 {
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 {
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)
})
}
}
07070100000019000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000002000000000frizbee-0.1.7/internal/traverse0707010000001A000081A400000000000000000000000167E26F5900001229000000000000000000000000000000000000002C00000000frizbee-0.1.7/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
}
0707010000001B000081A400000000000000000000000167E26F59000014D2000000000000000000000000000000000000003100000000frizbee-0.1.7/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 {
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 {
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 {
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 }
0707010000001C000081A400000000000000000000000167E26F5900000089000000000000000000000000000000000000001600000000frizbee-0.1.7/main.go/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package main
import "github.com/stacklok/frizbee/cmd"
func main() {
cmd.Execute()
}
0707010000001D000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001200000000frizbee-0.1.7/pkg0707010000001E000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001D00000000frizbee-0.1.7/pkg/interfaces0707010000001F000081A400000000000000000000000167E26F5900000747000000000000000000000000000000000000002B00000000frizbee-0.1.7/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)
}
07070100000020000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001B00000000frizbee-0.1.7/pkg/replacer07070100000021000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000002300000000frizbee-0.1.7/pkg/replacer/actions07070100000022000081A400000000000000000000000167E26F5900002BD1000000000000000000000000000000000000002E00000000frizbee-0.1.7/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/v66/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)
}
sha, otype, err := doGetReference(ctx, restIf, path)
if err != nil {
return "", err
}
if otype == "commit" {
return sha, nil
}
// assume otype == "tag"
path, err = url.JoinPath("repos", owner, repo, "git", "tags", sha)
if err != nil {
return "", fmt.Errorf("failed to join path: %w", err)
}
sha, _, err = doGetReference(ctx, restIf, path)
return sha, err
}
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)
}
sha, _, err := doGetReference(ctx, restIf, path)
return sha, err
}
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, 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(), t.GetObject().GetType(), nil
}
07070100000023000081A400000000000000000000000167E26F5900001FCE000000000000000000000000000000000000003300000000frizbee-0.1.7/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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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)
})
}
}
07070100000024000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000002100000000frizbee-0.1.7/pkg/replacer/image07070100000025000081A400000000000000000000000167E26F5900002043000000000000000000000000000000000000002A00000000frizbee-0.1.7/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
}
07070100000026000081A400000000000000000000000167E26F59000017C4000000000000000000000000000000000000002F00000000frizbee-0.1.7/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 {
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 {
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 {
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 {
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 {
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 {
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 {
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")
})
}
}
07070100000027000081A400000000000000000000000167E26F5900002846000000000000000000000000000000000000002700000000frizbee-0.1.7/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
}
07070100000028000081A400000000000000000000000167E26F5900008940000000000000000000000000000000000000002C00000000frizbee-0.1.7/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 {
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 {
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 {
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@b643b0751c371f357690337d4549221347c0e1bc # 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@b643b0751c371f357690337d4549221347c0e1bc # 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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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@b643b0751c371f357690337d4549221347c0e1bc # v1
`,
expected: &ListResult{
Entities: []interfaces.EntityRef{
{
Name: "actions/checkout",
Ref: "ee0669bd1cc54295c223e0bb666b733df41de1c5",
Type: actions.ReferenceType,
},
{
Name: "xt0rted/markdownlint-problem-matcher",
Ref: "b643b0751c371f357690337d4549221347c0e1bc",
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 {
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)
}
})
}
}
07070100000029000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001800000000frizbee-0.1.7/pkg/utils0707010000002A000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001F00000000frizbee-0.1.7/pkg/utils/config0707010000002B000081A400000000000000000000000167E26F590000114D000000000000000000000000000000000000002900000000frizbee-0.1.7/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
}
0707010000002C000081A400000000000000000000000167E26F59000010DC000000000000000000000000000000000000002E00000000frizbee-0.1.7/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 {
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: "DontIgnoreBranches",
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 {
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)
}
}
})
}
}
0707010000002D000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001F00000000frizbee-0.1.7/pkg/utils/ghrest0707010000002E000081A400000000000000000000000167E26F5900000844000000000000000000000000000000000000002900000000frizbee-0.1.7/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/v66/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
}
0707010000002F000081A400000000000000000000000167E26F5900000AC7000000000000000000000000000000000000002E00000000frizbee-0.1.7/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 {
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
}
}
07070100000030000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001E00000000frizbee-0.1.7/pkg/utils/store07070100000031000081A400000000000000000000000167E26F5900000702000000000000000000000000000000000000002700000000frizbee-0.1.7/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
}
07070100000032000081A400000000000000000000000167E26F59000009B2000000000000000000000000000000000000002C00000000frizbee-0.1.7/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 {
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!!!372 blocks