pax_global_header00006660000000000000000000000064146705312100014511gustar00rootroot0000000000000052 comment=06acb350e4aa5ddf4f68f5eaddcda41a4dd99cc1 release-utils-0.8.5/000077500000000000000000000000001467053121000143015ustar00rootroot00000000000000release-utils-0.8.5/.bom.yaml000066400000000000000000000001421467053121000160150ustar00rootroot00000000000000--- license: Apache-2.0 name: sigs.k8s.io/release-utils creator: person: The Kubernetes Authors release-utils-0.8.5/.github/000077500000000000000000000000001467053121000156415ustar00rootroot00000000000000release-utils-0.8.5/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000045621467053121000214510ustar00rootroot00000000000000 #### What type of PR is this? #### What this PR does / why we need it: #### Which issue(s) this PR fixes: #### Special notes for your reviewer: #### Does this PR introduce a user-facing change? ```release-note ``` release-utils-0.8.5/.github/dependabot.yml000066400000000000000000000004651467053121000204760ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: gomod directory: "/" schedule: interval: daily labels: - "area/dependency" - "release-note-none" - "ok-to-test" open-pull-requests-limit: 10 groups: all: update-types: - "minor" - "patch" release-utils-0.8.5/.github/workflows/000077500000000000000000000000001467053121000176765ustar00rootroot00000000000000release-utils-0.8.5/.github/workflows/release.yaml000066400000000000000000000023441467053121000222050ustar00rootroot00000000000000name: Release on: push: tags: - 'v*' jobs: release: runs-on: ubuntu-latest permissions: contents: write # needed to write releases steps: - name: Set tag name shell: bash run: | echo "TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Check out code uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 1 - name: Set up go uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v3 with: go-version-file: go.mod check-latest: true cache: false - name: Install bom uses: kubernetes-sigs/release-actions/setup-bom@2f8b9ec22aedc9ce15039b6c7716aa6c2907df1c # v0.2.0 - name: Generate SBOM shell: bash run: | bom generate -c .bom.yaml --format=json -o /tmp/sigs.k8s.io-release-utils-$TAG.spdx.json . - name: Publish Release uses: kubernetes-sigs/release-actions/publish-release@2f8b9ec22aedc9ce15039b6c7716aa6c2907df1c # v0.2.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: assets: "/tmp/sigs.k8s.io-release-utils-$TAG.spdx.json" sbom: false release-utils-0.8.5/.gitignore000066400000000000000000000042521467053121000162740ustar00rootroot00000000000000# NFS .nfs* # OSX leaves these everywhere on SMB shares ._* # OSX trash .DS_Store # Eclipse files .classpath .project .settings/** # Files generated by JetBrains IDEs, e.g. IntelliJ IDEA .idea/ *.iml # Vscode files .vscode # This is where the result of the go build goes /output*/ /_output*/ /_output # Emacs save files *~ \#*\# .\#* # Vim-related files [._]*.s[a-w][a-z] [._]s[a-w][a-z] *.un~ Session.vim .netrwhist # Go test binaries *.test /hack/.test-cmd-auth # JUnit test output from ginkgo e2e tests /junit*.xml # Mercurial files **/.hg **/.hg* # Vagrant .vagrant network_closure.sh # Local cluster env variables /cluster/env.sh # Compiled binaries in third_party /third_party/pkg # Also ignore etcd installed by hack/install-etcd.sh /third_party/etcd* # User cluster configs .kubeconfig .tags* # Version file for dockerized build .dockerized-kube-version-defs # Web UI /www/master/node_modules/ /www/master/npm-debug.log /www/master/shared/config/development.json # Karma output /www/test_out # precommit temporary directories created by ./hack/verify-generated-docs.sh and ./hack/lib/util.sh /_tmp/ /doc_tmp/ # Test artifacts produced by Jenkins jobs /_artifacts/ # Go dependencies installed on Jenkins /_gopath/ # Config directories created by gcloud and gsutil on Jenkins /.config/gcloud*/ /.gsutil/ # CoreOS stuff /cluster/libvirt-coreos/coreos_*.img # Juju Stuff /cluster/juju/charms/* /cluster/juju/bundles/local.yaml # Downloaded Kubernetes binary release /kubernetes/ # direnv .envrc files .envrc # Downloaded kubernetes binary release tar ball kubernetes.tar.gz # generated files in any directory # TODO(thockin): uncomment this when we stop committing the generated files. #zz_generated.* # make-related metadata /.make/ # Just in time generated data in the source, should never be committed /test/e2e/generated/bindata.go # This file used by some vendor repos (e.g. github.com/go-openapi/...) to store secret variables and should not be ignored !\.drone\.sec /bazel-* # vendored go modules /vendor # git merge conflict originals *.orig # go coverage files coverage.* # test files tmp CHANGELOG-*.html # downloaded and built binaries bin qemu-*-static rootfs.tar release-utils-0.8.5/.golangci.yml000066400000000000000000000115051467053121000166670ustar00rootroot00000000000000--- run: concurrency: 6 timeout: 5m issues: exclude-rules: # counterfeiter fakes are usually named 'fake_.go' - path: fake_.*\.go linters: - gocritic - golint - dupl # Maximum issues count per one linter. Set to 0 to disable. Default is 50. max-issues-per-linter: 0 # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. max-same-issues: 0 linters: disable-all: true enable: - asasalint - asciicheck - bidichk - bodyclose - canonicalheader - contextcheck - copyloopvar - decorder - dogsled - dupl - durationcheck - errcheck - errchkjson - errname - fatcontext - gci - ginkgolinter - gocheckcompilerdirectives - gochecksumtype - goconst - gocritic - gocyclo - godot - godox - gofmt - gofumpt - goheader - goimports - gomoddirectives - gomodguard - goprintffuncname - gosec - gosimple - gosmopolitan - govet - grouper - importas - ineffassign - intrange - loggercheck - makezero - mirror - misspell - musttag - nakedret - nolintlint - nosprintfhostport - perfsprint - prealloc - predeclared - promlinter - protogetter - reassign - revive - rowserrcheck - sloglint - spancheck - sqlclosecheck - staticcheck - stylecheck - tagalign - tenv - testableexamples - testifylint - typecheck - unconvert - unparam - unused - usestdlibvars - whitespace - zerologlint # - containedctx # - cyclop # - depguard # - dupword # - err113 # - errorlint # - exhaustive # - exhaustruct # - forbidigo # - forcetypeassert # - funlen # - gochecknoglobals # - gochecknoinits # - gocognit # - inamedparam # - interfacebloat # - ireturn # - lll # - maintidx # - mnd # - nestif # - nilerr # - nilnil # - nlreturn # - noctx # - nonamedreturns # - paralleltest # - tagliatelle # - testpackage # - thelper # - tparallel # - varnamelen # - wastedassign # - wrapcheck # - wsl linters-settings: gci: sections: - standard - default - prefix(k8s.io) - prefix(sigs.k8s.io) - localmodule gocyclo: min-complexity: 40 godox: keywords: - BUG - FIXME - HACK errcheck: check-type-assertions: true check-blank: true gocritic: enabled-checks: - appendCombine - badLock - badRegexp - badSorting - badSyncOnceFunc - boolExprSimplify - builtinShadow - builtinShadowDecl - commentedOutCode - commentedOutImport - deferInLoop - deferUnlambda - docStub - dupImport - dynamicFmtString - emptyDecl - emptyFallthrough - emptyStringTest - equalFold - evalOrder - exposedSyncMutex - externalErrorReassign - filepathJoin - hexLiteral - httpNoBody - hugeParam - importShadow - indexAlloc - initClause - methodExprCall - nestingReduce - nilValReturn - octalLiteral - paramTypeCombine - preferDecodeRune - preferFilepathJoin - preferFprint - preferStringWriter - preferWriteByte - ptrToRefParam - rangeExprCopy - rangeValCopy - redundantSprint - regexpPattern - regexpSimplify - returnAfterHttpError - ruleguard - sliceClear - sloppyReassign - sortSlice - sprintfQuotedString - sqlQuery - stringConcatSimplify - stringXbytes - stringsCompare - syncMapLoadAndDelete - timeExprSimplify - todoCommentWithoutDetail - tooManyResultsChecker - truncateCmp - typeAssertChain - typeDefFirst - typeUnparen - uncheckedInlineErr - unlabelStmt - unnamedResult - unnecessaryBlock - unnecessaryDefer - weakCond - yodaStyleExpr # - whyNoLint nolintlint: # Enable to ensure that nolint directives are all used. Default is true. allow-unused: false # Disable to ensure that nolint directives don't have a leading space. Default is true. # TODO(lint): Enforce machine-readable `nolint` directives allow-leading-space: true # Exclude following linters from requiring an explanation. Default is []. allow-no-explanation: [] # Enable to require an explanation of nonzero length after each nolint directive. Default is false. # TODO(lint): Enforce explanations for `nolint` directives require-explanation: false # Enable to require nolint directives to mention the specific linter being suppressed. Default is false. require-specific: true release-utils-0.8.5/CONTRIBUTING.md000066400000000000000000000035101467053121000165310ustar00rootroot00000000000000# Contributing Guidelines Welcome to Kubernetes. We are excited about the prospect of you joining our [community](https://git.k8s.io/community)! The Kubernetes community abides by the CNCF [code of conduct](code-of-conduct.md). Here is an excerpt: _As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities._ ## Getting Started We have full documentation on how to get started contributing here: - [Contributor License Agreement](https://git.k8s.io/community/CLA.md) Kubernetes projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests - [Kubernetes Contributor Guide](https://git.k8s.io/community/contributors/guide) - Main contributor documentation, or you can just jump directly to the [contributing section](https://git.k8s.io/community/contributors/guide#contributing) - [Contributor Cheat Sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet) - Common resources for existing developers ## Mentorship - [Mentoring Initiatives](https://git.k8s.io/community/mentoring) - We have a diverse set of mentorship programs available that are always looking for volunteers! release-utils-0.8.5/LICENSE000066400000000000000000000261351467053121000153150ustar00rootroot00000000000000 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 {yyyy} {name of copyright owner} 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. release-utils-0.8.5/OWNERS000066400000000000000000000003621467053121000152420ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners approvers: - sig-release-leads - release-engineering-approvers reviewers: - release-engineering-approvers - release-engineering-reviewers labels: - sig/release - area/release-eng release-utils-0.8.5/OWNERS_ALIASES000066400000000000000000000016631467053121000164100ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners aliases: sig-release-leads: - cpanato # SIG Technical Lead - jeremyrickard # SIG Chair - justaugustus # SIG Chair - puerco # SIG Technical Lead - saschagrunert # SIG Chair - Verolop # SIG Technical Lead release-engineering-approvers: - cpanato # subproject owner / Release Manager - jeremyrickard # subproject owner / Release Manager - justaugustus # subproject owner / Release Manager - palnabarun # Release Manager - puerco # subproject owner / Release Manager - saschagrunert # subproject owner / Release Manager - xmudrii # Release Manager - Verolop # subproject owner / Release Manager release-engineering-reviewers: - ameukam # Release Manager Associate - cici37 # Release Manager Associate - jimangel # Release Manager Associate - jrsapi # Release Manager Associate - salaxander # Release Manager Associate release-utils-0.8.5/README.md000066400000000000000000000002721467053121000155610ustar00rootroot00000000000000# release-utils Tiny utilities for use by the Release Engineering subproject and [kubernetes/release](https://github.com/kubernetes/release/). Hopefully they can be useful to you too! release-utils-0.8.5/SECURITY.md000066400000000000000000000020551467053121000160740ustar00rootroot00000000000000# Security Policy ## Security Announcements Join the [kubernetes-security-announce] group for security and vulnerability announcements. You can also subscribe to an RSS feed of the above using [this link][kubernetes-security-announce-rss]. ## Reporting a Vulnerability Instructions for reporting a vulnerability can be found on the [Kubernetes Security and Disclosure Information] page. ## Supported Versions Information about supported Kubernetes versions can be found on the [Kubernetes version and version skew support policy] page on the Kubernetes website. [kubernetes-security-announce]: https://groups.google.com/forum/#!forum/kubernetes-security-announce [kubernetes-security-announce-rss]: https://groups.google.com/forum/feed/kubernetes-security-announce/msgs/rss_v2_0.xml?num=50 [Kubernetes version and version skew support policy]: https://kubernetes.io/docs/setup/release/version-skew-policy/#supported-versions [Kubernetes Security and Disclosure Information]: https://kubernetes.io/docs/reference/issues-security/security/#report-a-vulnerability release-utils-0.8.5/SECURITY_CONTACTS000066400000000000000000000010711467053121000167700ustar00rootroot00000000000000# Defined below are the security contacts for this repo. # # They are the contact point for the Product Security Committee to reach out # to for triaging and handling of incoming issues. # # The below names agree to abide by the # [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy) # and will be removed and replaced if they violate that agreement. # # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE # INSTRUCTIONS AT https://kubernetes.io/security/ hasheddan jeremyrickard justaugustus saschagrunert release-utils-0.8.5/code-of-conduct.md000066400000000000000000000002241467053121000175720ustar00rootroot00000000000000# Kubernetes Community Code of Conduct Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) release-utils-0.8.5/command/000077500000000000000000000000001467053121000157175ustar00rootroot00000000000000release-utils-0.8.5/command/command.go000066400000000000000000000301541467053121000176670ustar00rootroot00000000000000/* Copyright 2019 The Kubernetes Authors. 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 command import ( "bytes" "fmt" "io" "os" "os/exec" "regexp" "strings" "sync" "syscall" "github.com/sirupsen/logrus" ) // A generic command abstraction. type Command struct { cmds []*command stdErrWriters, stdOutWriters []io.Writer env []string verbose bool filter *filter } // The internal command representation. type command struct { *exec.Cmd pipeWriter *io.PipeWriter } // filter is the internally used struct for filtering command output. type filter struct { regex *regexp.Regexp replaceAll string } // A generic command exit status. type Status struct { waitStatus syscall.WaitStatus *Stream } // Stream combines standard output and error. type Stream struct { //nolint: errname stdOut string stdErr string } // Commands is an abstraction over multiple Command structures. type Commands []*Command // New creates a new command from the provided arguments. func New(cmd string, args ...string) *Command { return NewWithWorkDir("", cmd, args...) } // NewWithWorkDir creates a new command from the provided workDir and the command // arguments. func NewWithWorkDir(workDir, cmd string, args ...string) *Command { return &Command{ cmds: []*command{{ Cmd: cmdWithDir(workDir, cmd, args...), pipeWriter: nil, }}, stdErrWriters: []io.Writer{}, stdOutWriters: []io.Writer{}, verbose: false, } } func cmdWithDir(dir, cmd string, args ...string) *exec.Cmd { c := exec.Command(cmd, args...) c.Dir = dir return c } // Pipe creates a new command where the previous should be piped to. func (c *Command) Pipe(cmd string, args ...string) *Command { pipeCmd := cmdWithDir(c.cmds[0].Dir, cmd, args...) reader, writer := io.Pipe() c.cmds[len(c.cmds)-1].Stdout = writer pipeCmd.Stdin = reader c.cmds = append(c.cmds, &command{ Cmd: pipeCmd, pipeWriter: writer, }) return c } // Env specifies the environment added to the command. Each entry is of the // form "key=value". The environment of the current process is being preserved, // while it is possible to overwrite already existing environment variables. func (c *Command) Env(env ...string) *Command { c.env = append(c.env, env...) return c } // Verbose enables verbose output aka printing the command before executing it. func (c *Command) Verbose() *Command { c.verbose = true return c } // isVerbose returns true if the command is in verbose mode, either set locally // or global. func (c *Command) isVerbose() bool { return GetGlobalVerbose() || c.verbose } // Add a command with the same working directory as well as verbosity mode. // Returns a new Commands instance. func (c *Command) Add(cmd string, args ...string) Commands { addCmd := NewWithWorkDir(c.cmds[0].Dir, cmd, args...) addCmd.verbose = c.verbose addCmd.filter = c.filter return Commands{c, addCmd} } // AddWriter can be used to add an additional output (stdout) and error // (stderr) writer to the command, for example when having the need to log to // files. func (c *Command) AddWriter(writer io.Writer) *Command { c.AddOutputWriter(writer) c.AddErrorWriter(writer) return c } // AddErrorWriter can be used to add an additional error (stderr) writer to the // command, for example when having the need to log to files. func (c *Command) AddErrorWriter(writer io.Writer) *Command { c.stdErrWriters = append(c.stdErrWriters, writer) return c } // AddOutputWriter can be used to add an additional output (stdout) writer to // the command, for example when having the need to log to files. func (c *Command) AddOutputWriter(writer io.Writer) *Command { c.stdOutWriters = append(c.stdOutWriters, writer) return c } // Filter adds an output filter regular expression to the command. Every output // will then be replaced with the string provided by replaceAll. func (c *Command) Filter(regex, replaceAll string) (*Command, error) { filterRegex, err := regexp.Compile(regex) if err != nil { return nil, fmt.Errorf("compile regular expression: %w", err) } c.filter = &filter{ regex: filterRegex, replaceAll: replaceAll, } return c, nil } // Run starts the command and waits for it to finish. It returns an error if // the command execution was not possible at all, otherwise the Status. // This method prints the commands output during execution. func (c *Command) Run() (res *Status, err error) { return c.run(true) } // RunSuccessOutput starts the command and waits for it to finish. It returns // an error if the command execution was not successful, otherwise its output. func (c *Command) RunSuccessOutput() (output *Stream, err error) { res, err := c.run(true) if err != nil { return nil, err } if !res.Success() { return nil, fmt.Errorf("command %v did not succeed: %v", c.String(), res.Error()) } return res.Stream, nil } // RunSuccess starts the command and waits for it to finish. It returns an // error if the command execution was not successful. func (c *Command) RunSuccess() error { _, err := c.RunSuccessOutput() //nolint: errcheck return err } // String returns a string representation of the full command. func (c *Command) String() string { str := []string{} for _, x := range c.cmds { // Note: the following logic can be replaced with x.String(), which was // implemented in go1.13 b := new(strings.Builder) b.WriteString(x.Path) for _, a := range x.Args[1:] { b.WriteByte(' ') b.WriteString(a) } str = append(str, b.String()) } return strings.Join(str, " | ") } // Run starts the command and waits for it to finish. It returns an error if // the command execution was not possible at all, otherwise the Status. // This method does not print the output of the command during its execution. func (c *Command) RunSilent() (res *Status, err error) { return c.run(false) } // RunSilentSuccessOutput starts the command and waits for it to finish. It // returns an error if the command execution was not successful, otherwise its // output. This method does not print the output of the command during its // execution. func (c *Command) RunSilentSuccessOutput() (output *Stream, err error) { res, err := c.run(false) if err != nil { return nil, err } if !res.Success() { return nil, fmt.Errorf("command %v did not succeed: %w", c.String(), res) } return res.Stream, nil } // RunSilentSuccess starts the command and waits for it to finish. It returns // an error if the command execution was not successful. This method does not // print the output of the command during its execution. func (c *Command) RunSilentSuccess() error { _, err := c.RunSilentSuccessOutput() //nolint: errcheck return err } // run is the internal run method. func (c *Command) run(printOutput bool) (res *Status, err error) { var runErr error stdOutBuffer := &bytes.Buffer{} stdErrBuffer := &bytes.Buffer{} status := &Status{Stream: &Stream{}} type done struct { stdout error stderr error } doneChan := make(chan done, 1) var stdOutWriter io.Writer for i, cmd := range c.cmds { // Last command handling if i+1 == len(c.cmds) { stdout, err := cmd.StdoutPipe() if err != nil { return nil, err } stderr, err := cmd.StderrPipe() if err != nil { return nil, err } var stdErrWriter io.Writer if printOutput { stdOutWriter = io.MultiWriter(append( []io.Writer{os.Stdout, stdOutBuffer}, c.stdOutWriters..., )...) stdErrWriter = io.MultiWriter(append( []io.Writer{os.Stderr, stdErrBuffer}, c.stdErrWriters..., )...) } else { stdOutWriter = stdOutBuffer stdErrWriter = stdErrBuffer } go func() { var stdoutErr, stderrErr error wg := sync.WaitGroup{} wg.Add(2) filterCopy := func(read io.ReadCloser, write io.Writer) (err error) { if c.filter != nil { builder := &strings.Builder{} _, err = io.Copy(builder, read) if err != nil { return err } str := c.filter.regex.ReplaceAllString( builder.String(), c.filter.replaceAll, ) _, err = io.Copy(write, strings.NewReader(str)) } else { _, err = io.Copy(write, read) } return err } go func() { stdoutErr = filterCopy(stdout, stdOutWriter) wg.Done() }() go func() { stderrErr = filterCopy(stderr, stdErrWriter) wg.Done() }() wg.Wait() doneChan <- done{stdoutErr, stderrErr} }() } if c.isVerbose() { logrus.Infof("+ %s", c.String()) } cmd.Env = append(os.Environ(), c.env...) if err := cmd.Start(); err != nil { return nil, err } if i > 0 { if err := c.cmds[i-1].Wait(); err != nil { return nil, err } } if cmd.pipeWriter != nil { if err := cmd.pipeWriter.Close(); err != nil { return nil, err } } // Wait for last command in the pipe to finish if i+1 == len(c.cmds) { err := <-doneChan if err.stdout != nil && strings.Contains(err.stdout.Error(), os.ErrClosed.Error()) { return nil, fmt.Errorf("unable to copy stdout: %w", err.stdout) } if err.stderr != nil && strings.Contains(err.stderr.Error(), os.ErrClosed.Error()) { return nil, fmt.Errorf("unable to copy stderr: %w", err.stderr) } runErr = cmd.Wait() } } status.stdOut = stdOutBuffer.String() status.stdErr = stdErrBuffer.String() if exitErr, ok := runErr.(*exec.ExitError); ok { if waitStatus, ok := exitErr.Sys().(syscall.WaitStatus); ok { status.waitStatus = waitStatus return status, nil } } return status, runErr } // Success returns if a Status was successful. func (s *Status) Success() bool { return s.waitStatus.ExitStatus() == 0 } // ExitCode returns the exit status of the command status. func (s *Status) ExitCode() int { return s.waitStatus.ExitStatus() } // Output returns stdout of the command status. func (s *Stream) Output() string { return s.stdOut } // OutputTrimNL returns stdout of the command status with newlines trimmed // Use only when output is expected to be a single "word", like a version string. func (s *Stream) OutputTrimNL() string { return strings.TrimSpace(s.stdOut) } // Error returns the stderr of the command status. func (s *Stream) Error() string { return s.stdErr } // Execute is a convenience function which creates a new Command, executes it // and evaluates its status. func Execute(cmd string, args ...string) error { status, err := New(cmd, args...).Run() if err != nil { return fmt.Errorf("command %q is not executable: %w", cmd, err) } if !status.Success() { return fmt.Errorf( "command %q did not exit successful (%d)", cmd, status.ExitCode(), ) } return nil } // Available verifies that the specified `commands` are available within the // current `$PATH` environment and returns true if so. The function does not // check for duplicates nor if the provided slice is empty. func Available(commands ...string) (ok bool) { ok = true for _, command := range commands { if _, err := exec.LookPath(command); err != nil { logrus.Warnf("Unable to %v", err) ok = false } } return ok } // Add adds another command with the same working directory as well as // verbosity mode to the Commands. func (c Commands) Add(cmd string, args ...string) Commands { addCmd := NewWithWorkDir(c[0].cmds[0].Dir, cmd, args...) addCmd.verbose = c[0].verbose addCmd.filter = c[0].filter return append(c, addCmd) } // Run executes all commands sequentially and abort if any of those fails. func (c Commands) Run() (*Status, error) { res := &Status{Stream: &Stream{}} for _, cmd := range c { output, err := cmd.RunSuccessOutput() if err != nil { return nil, fmt.Errorf("running command %q: %w", cmd.String(), err) } res.stdOut += "\n" + output.stdOut res.stdErr += "\n" + output.stdErr } return res, nil } release-utils-0.8.5/command/command_test.go000066400000000000000000000225731467053121000207340ustar00rootroot00000000000000/* Copyright 2019 The Kubernetes Authors. 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 command import ( "bytes" "os" "testing" "github.com/stretchr/testify/require" ) func TestSuccess(t *testing.T) { res, err := New("echo", "hi").Run() require.NoError(t, err) require.True(t, res.Success()) require.Zero(t, res.ExitCode()) } func TestSuccessPipe(t *testing.T) { res, err := New("echo", "-n", "hi"). Pipe("cat"). Pipe("cat"). Run() require.NoError(t, err) require.True(t, res.Success()) require.Zero(t, res.ExitCode()) require.Equal(t, "hi", res.Output()) } func TestFailurePipeWrongCommand(t *testing.T) { res, err := New("echo", "-n", "hi"). Pipe("wrong"). Run() require.Error(t, err) require.Nil(t, res) } func TestFailurePipeWrongArgument(t *testing.T) { res, err := New("echo", "-n", "hi"). Pipe("cat", "--wrong"). Run() require.NoError(t, err) require.False(t, res.Success()) require.Empty(t, res.Output()) require.NotEmpty(t, res.Error()) } func TestSuccessVerbose(t *testing.T) { res, err := New("echo", "hi").Verbose().Run() require.NoError(t, err) require.True(t, res.Success()) require.Zero(t, res.ExitCode()) } func TestSuccessWithWorkingDir(t *testing.T) { res, err := NewWithWorkDir("/", "ls", "-1").Run() require.NoError(t, err) require.True(t, res.Success()) require.Zero(t, res.ExitCode()) } func TestFailureWithWrongWorkingDir(t *testing.T) { res, err := NewWithWorkDir("/should/not/exist", "ls", "-1").Run() require.Error(t, err) require.Nil(t, res) } func TestSuccessSilent(t *testing.T) { res, err := New("echo", "hi").RunSilent() require.NoError(t, err) require.True(t, res.Success()) } func TestSuccessSeparated(t *testing.T) { res, err := New("echo", "hi").RunSilent() require.NoError(t, err) require.True(t, res.Success()) } func TestSuccessSingleArgument(t *testing.T) { res, err := New("echo").Run() require.NoError(t, err) require.True(t, res.Success()) } func TestSuccessNoArgument(t *testing.T) { res, err := New("").Run() require.Error(t, err) require.Nil(t, res) } func TestSuccessOutput(t *testing.T) { res, err := New("echo", "-n", "hello world").Run() require.NoError(t, err) require.Equal(t, "hello world", res.Output()) } func TestSuccessOutputTrimNL(t *testing.T) { res, err := New("echo", "-n", "hello world\n").Run() require.NoError(t, err) require.Equal(t, "hello world", res.OutputTrimNL()) } func TestSuccessError(t *testing.T) { res, err := New("cat", "/not/valid").Run() require.NoError(t, err) require.Empty(t, res.Output()) require.Contains(t, res.Error(), "No such file") } func TestSuccessOutputSeparated(t *testing.T) { res, err := New("echo", "-n", "hello").Run() require.NoError(t, err) require.Equal(t, "hello", res.Output()) } func TestFailureStdErr(t *testing.T) { res, err := New("cat", "/not/valid").Run() require.NoError(t, err) require.False(t, res.Success()) require.Equal(t, 1, res.ExitCode()) } func TestFailureNotExisting(t *testing.T) { res, err := New("/not/valid").Run() require.Error(t, err) require.Nil(t, res) } func TestSuccessExecute(t *testing.T) { err := Execute("echo", "-n", "hi", "ho") require.NoError(t, err) } func TestFailureExecute(t *testing.T) { err := Execute("cat", "/not/invalid") require.Error(t, err) } func TestAvailableSuccessValidCommand(t *testing.T) { res := Available("echo") require.True(t, res) } func TestAvailableSuccessEmptyCommands(t *testing.T) { res := Available() require.True(t, res) } func TestAvailableFailure(t *testing.T) { res := Available("echo", "this-command-should-not-exist") require.False(t, res) } func TestSuccessRunSuccess(t *testing.T) { require.NoError(t, New("echo", "hi").RunSuccess()) } func TestFailureRunSuccess(t *testing.T) { require.Error(t, New("cat", "/not/available").RunSuccess()) } func TestSuccessRunSilentSuccess(t *testing.T) { require.NoError(t, New("echo", "hi").RunSilentSuccess()) } func TestFailureRunSuccessSilent(t *testing.T) { require.Error(t, New("cat", "/not/available").RunSilentSuccess()) } func TestSuccessRunSuccessOutput(t *testing.T) { res, err := New("echo", "-n", "hi").RunSuccessOutput() require.NoError(t, err) require.Equal(t, "hi", res.Output()) } func TestFailureRunSuccessOutput(t *testing.T) { res, err := New("cat", "/not/available").RunSuccessOutput() require.Error(t, err) require.Nil(t, res) } func TestSuccessRunSilentSuccessOutput(t *testing.T) { res, err := New("echo", "-n", "hi").RunSilentSuccessOutput() require.NoError(t, err) require.Equal(t, "hi", res.Output()) } func TestFailureRunSilentSuccessOutput(t *testing.T) { res, err := New("cat", "/not/available").RunSilentSuccessOutput() require.Error(t, err) require.Nil(t, res) } func TestSuccessLogWriter(t *testing.T) { f, err := os.CreateTemp("", "log") require.NoError(t, err) defer func() { require.NoError(t, os.Remove(f.Name())) }() res, err := New("echo", "Hello World").AddWriter(f).RunSuccessOutput() require.NoError(t, err) content, err := os.ReadFile(f.Name()) require.NoError(t, err) require.Equal(t, res.Output(), string(content)) } func TestSuccessLogWriterMultiple(t *testing.T) { f, err := os.CreateTemp("", "log") require.NoError(t, err) defer func() { require.NoError(t, os.Remove(f.Name())) }() b := &bytes.Buffer{} res, err := New("echo", "Hello World"). AddWriter(f). AddWriter(b). RunSuccessOutput() require.NoError(t, err) content, err := os.ReadFile(f.Name()) require.NoError(t, err) require.Equal(t, res.Output(), string(content)) require.Equal(t, res.Output(), b.String()) } func TestSuccessLogWriterSilent(t *testing.T) { f, err := os.CreateTemp("", "log") require.NoError(t, err) defer func() { require.NoError(t, os.Remove(f.Name())) }() err = New("echo", "Hello World").AddWriter(f).RunSilentSuccess() require.NoError(t, err) content, err := os.ReadFile(f.Name()) require.NoError(t, err) require.Empty(t, content) } func TestSuccessLogWriterStdErr(t *testing.T) { f, err := os.CreateTemp("", "log") require.NoError(t, err) defer func() { require.NoError(t, os.Remove(f.Name())) }() res, err := New("bash", "-c", ">&2 echo error"). AddWriter(f).RunSuccessOutput() require.NoError(t, err) content, err := os.ReadFile(f.Name()) require.NoError(t, err) require.Equal(t, res.Error(), string(content)) } func TestSuccessLogWriterStdErrAndStdOut(t *testing.T) { f, err := os.CreateTemp("", "log") require.NoError(t, err) defer func() { require.NoError(t, os.Remove(f.Name())) }() res, err := New("bash", "-c", ">&2 echo stderr; echo stdout"). AddWriter(f).RunSuccessOutput() require.NoError(t, err) content, err := os.ReadFile(f.Name()) require.NoError(t, err) require.Contains(t, string(content), res.Output()) require.Contains(t, string(content), res.Error()) } func TestSuccessLogWriterStdErrAndStdOutOnlyStdErr(t *testing.T) { f, err := os.CreateTemp("", "log") require.NoError(t, err) defer func() { require.NoError(t, os.Remove(f.Name())) }() res, err := New("bash", "-c", ">&2 echo stderr; echo stdout"). AddErrorWriter(f).RunSuccessOutput() require.NoError(t, err) content, err := os.ReadFile(f.Name()) require.NoError(t, err) require.Equal(t, res.Error(), string(content)) } func TestSuccessLogWriterStdErrAndStdOutOnlyStdOut(t *testing.T) { f, err := os.CreateTemp("", "log") require.NoError(t, err) defer func() { require.NoError(t, os.Remove(f.Name())) }() res, err := New("bash", "-c", ">&2 echo stderr; echo stdout"). AddOutputWriter(f).RunSuccessOutput() require.NoError(t, err) content, err := os.ReadFile(f.Name()) require.NoError(t, err) require.Equal(t, res.Output(), string(content)) } func TestCommandsSuccess(t *testing.T) { res, err := New("echo", "1").Verbose(). Add("echo", "2").Add("echo", "3").Run() require.NoError(t, err) require.True(t, res.Success()) require.Zero(t, res.ExitCode()) require.Contains(t, res.Output(), "1") require.Contains(t, res.Output(), "2") require.Contains(t, res.Output(), "3") } func TestCommandsFailure(t *testing.T) { res, err := New("echo", "1").Add("wrong").Add("echo", "3").Run() require.Error(t, err) require.Nil(t, res) } func TestEnv(t *testing.T) { t.Setenv("ABC", "test") // preserved t.Setenv("FOO", "test") // overwritten res, err := New("sh", "-c", "echo $TEST; echo $FOO; echo $ABC"). Env("TEST=123"). Env("FOO=bar"). RunSuccessOutput() require.NoError(t, err) require.Equal(t, "123\nbar\ntest", res.OutputTrimNL()) } func TestFilterStdout(t *testing.T) { cmd, err := New("echo", "-n", "1 2 2 3").Filter("[25]", "0") require.NoError(t, err) res, err := cmd.Add("echo", "-n", "4 5 6 2 2").Run() require.NoError(t, err) require.True(t, res.Success()) require.Zero(t, res.ExitCode()) require.Equal(t, "\n1 0 0 3\n4 0 6 0 0", res.Output()) } func TestFilterStderr(t *testing.T) { res, err := New("bash", "-c", ">&2 echo -n my secret").Filter("secret", "***") require.NoError(t, err) out, err := res.RunSilentSuccessOutput() require.NoError(t, err) require.Equal(t, "my ***", out.Error()) require.Empty(t, out.Output()) } release-utils-0.8.5/command/global.go000066400000000000000000000021071467053121000175060ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. 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 command import ( "sync/atomic" ) // atomicInt is the global variable for storing the globally set verbosity // level. It should never be used directly to avoid data races. var atomicInt int32 // SetGlobalVerbose sets the global command verbosity to the specified value. func SetGlobalVerbose(to bool) { var i int32 if to { i = 1 } atomic.StoreInt32(&atomicInt, i) } // GetGlobalVerbose returns the globally set command verbosity. func GetGlobalVerbose() bool { return atomic.LoadInt32(&atomicInt) != 0 } release-utils-0.8.5/command/global_test.go000066400000000000000000000014411467053121000205450ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. 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 command import ( "testing" "github.com/stretchr/testify/require" ) func TestSetGlobalVerboseSuccess(t *testing.T) { require.False(t, GetGlobalVerbose()) SetGlobalVerbose(true) require.True(t, GetGlobalVerbose()) } release-utils-0.8.5/dependencies.yaml000066400000000000000000000031751467053121000176210ustar00rootroot00000000000000dependencies: # golangci/golangci-lint - name: "golangci-lint" version: 1.61.0 refPaths: - path: mage/golangci-lint.go match: defaultGolangCILintVersion\s+=\s+"v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?" # ko - name: "ko" version: 0.15.2 refPaths: - path: mage/ko.go match: defaultKoVersion\s+=\s+"(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?" # cosign - name: "cosign" version: 2.2.4 refPaths: - path: mage/cosign.go match: defaultCosignVersion\s+=\s+"v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?" # k8s.io/repo-infra - name: "repo-infra" version: 0.2.5 refPaths: - path: mage/boilerplate.go match: defaultRepoInfraVersion\s+=\s+"v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))? # sigs.k8s.io/zeitgeist - name: "zeitgeist" version: 0.5.3 refPaths: - path: mage/dependency.go match: defaultZeitgeistVersion\s+=\s+"v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?" release-utils-0.8.5/editor/000077500000000000000000000000001467053121000155675ustar00rootroot00000000000000release-utils-0.8.5/editor/editor.go000066400000000000000000000110541467053121000174050ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. 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 editor import ( "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "strings" "github.com/sirupsen/logrus" ) const ( // sorry, blame Git // TODO: on Windows rely on 'start' to launch the editor associated // with the given file type. If we can't because of the need of // blocking, use a script with 'ftype' and 'assoc' to detect it. defaultEditor = "vi" defaultShell = "/bin/bash" windowsEditor = "notepad" windowsShell = "cmd" ) // Editor holds the command-line args to fire up the editor. type Editor struct { Args []string Shell bool } // NewDefaultEditor creates a struct Editor that uses the OS environment to // locate the editor program, looking at EDITOR environment variable to find // the proper command line. If the provided editor has no spaces, or no quotes, // it is treated as a bare command to be loaded. Otherwise, the string will // be passed to the user's shell for execution. func NewDefaultEditor(envs []string) Editor { args, shell := defaultEnvEditor(envs) return Editor{ Args: args, Shell: shell, } } func defaultEnvShell() []string { shell := os.Getenv("SHELL") if shell == "" { shell = platformize(defaultShell, windowsShell) } flag := "-c" if shell == windowsShell { flag = "/C" } return []string{shell, flag} } func defaultEnvEditor(envs []string) ([]string, bool) { var editor string for _, env := range envs { if env != "" { editor = os.Getenv(env) } if editor != "" { break } } if editor == "" { editor = platformize(defaultEditor, windowsEditor) } if !strings.Contains(editor, " ") { return []string{editor}, false } if !strings.ContainsAny(editor, "\"'\\") { return strings.Split(editor, " "), false } // rather than parse the shell arguments ourselves, punt to the shell shell := defaultEnvShell() return append(shell, editor), true } func (e Editor) args(path string) []string { args := make([]string, len(e.Args)) copy(args, e.Args) if e.Shell { last := args[len(args)-1] args[len(args)-1] = fmt.Sprintf("%s %q", last, path) } else { args = append(args, path) //nolint: makezero } return args } // Launch opens the described or returns an error. The TTY will be protected, and // SIGQUIT, SIGTERM, and SIGINT will all be trapped. func (e Editor) Launch(path string) error { if len(e.Args) == 0 { return fmt.Errorf("no editor defined, can't open %s", path) } abs, err := filepath.Abs(path) if err != nil { return err } args := e.args(abs) // TODO: check to validate the args and maybe sabitize those cmd := exec.Command(args[0], args[1:]...) //nolint: gosec cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin logrus.Infof("Opening file with editor %v", args) if err := (TTY{In: os.Stdin, TryDev: true}).Safe(cmd.Run); err != nil { if err, ok := err.(*exec.Error); ok { if err.Err == exec.ErrNotFound { return fmt.Errorf("unable to launch the editor %q", strings.Join(e.Args, " ")) } } return fmt.Errorf("there was a problem with the editor %q: %w", strings.Join(e.Args, " "), err) } return nil } // LaunchTempFile reads the provided stream into a temporary file in the given directory // and file prefix, and then invokes Launch with the path of that file. It will return // the contents of the file after launch, any errors that occur, and the path of the // temporary file so the caller can clean it up as needed. func (e Editor) LaunchTempFile(prefix, suffix string, r io.Reader) (bytes []byte, path string, err error) { f, err := os.CreateTemp("", fmt.Sprintf("%s*%s", prefix, suffix)) if err != nil { return nil, "", err } defer f.Close() path = f.Name() if _, err := io.Copy(f, r); err != nil { os.Remove(path) return nil, path, err } // This file descriptor needs to close so the next process (Launch) can claim it. f.Close() if err := e.Launch(path); err != nil { return nil, path, err } bytes, err = os.ReadFile(path) return bytes, path, err } func platformize(linux, windows string) string { if runtime.GOOS == "windows" { return windows } return linux } release-utils-0.8.5/editor/editor_test.go000066400000000000000000000040751467053121000204510ustar00rootroot00000000000000/* Copyright 2015 The Kubernetes Authors. 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 editor import ( "bytes" "os" "reflect" "strings" "testing" ) func TestArgs(t *testing.T) { if e, a := []string{"/bin/bash", "-c \"test\""}, (Editor{Args: []string{"/bin/bash", "-c"}, Shell: true}).args("test"); !reflect.DeepEqual(e, a) { t.Errorf("unexpected args: %v", a) } if e, a := []string{"/bin/bash", "-c", "test"}, (Editor{Args: []string{"/bin/bash", "-c"}, Shell: false}).args("test"); !reflect.DeepEqual(e, a) { t.Errorf("unexpected args: %v", a) } if e, a := []string{"/bin/bash", "-i -c \"test\""}, (Editor{Args: []string{"/bin/bash", "-i -c"}, Shell: true}).args("test"); !reflect.DeepEqual(e, a) { t.Errorf("unexpected args: %v", a) } if e, a := []string{"/test", "test"}, (Editor{Args: []string{"/test"}}).args("test"); !reflect.DeepEqual(e, a) { t.Errorf("unexpected args: %v", a) } } func TestEditor(t *testing.T) { edit := Editor{Args: []string{"cat"}} testStr := "test something\n" contents, path, err := edit.LaunchTempFile("", "someprefix", bytes.NewBufferString(testStr)) if err != nil { t.Fatalf("unexpected error: %v", err) } if _, err := os.Stat(path); err != nil { t.Fatalf("no temp file: %s", path) } defer os.Remove(path) if disk, err := os.ReadFile(path); err != nil || !bytes.Equal(contents, disk) { t.Errorf("unexpected file on disk: %v %s", err, string(disk)) } if !bytes.Equal(contents, []byte(testStr)) { t.Errorf("unexpected contents: %s", string(contents)) } if !strings.Contains(path, "someprefix") { t.Errorf("path not expected: %s", path) } } release-utils-0.8.5/editor/tty.go000066400000000000000000000044411467053121000167410ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. 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 editor import ( "io" "os" "github.com/moby/term" "github.com/sirupsen/logrus" ) // TTY helps invoke a function and preserve the state of the terminal, even if the process is // terminated during execution. It also provides support for terminal resizing for remote command // execution/attachment. type TTY struct { // In is a reader representing stdin. It is a required field. In io.Reader // Out is a writer representing stdout. It must be set to support terminal resizing. It is an // optional field. Out io.Writer // Raw is true if the terminal should be set raw. Raw bool // TryDev indicates the TTY should try to open /dev/tty if the provided input // is not a file descriptor. TryDev bool } // Safe invokes the provided function and will attempt to ensure that when the // function returns (or a termination signal is sent) that the terminal state // is reset to the condition it was in prior to the function being invoked. If // t.Raw is true the terminal will be put into raw mode prior to calling the function. // If the input file descriptor is not a TTY and TryDev is true, the /dev/tty file // will be opened (if available). func (t TTY) Safe(fn func() error) error { inFd, isTerminal := term.GetFdInfo(t.In) if !isTerminal && t.TryDev { if f, err := os.Open("/dev/tty"); err == nil { defer f.Close() inFd = f.Fd() isTerminal = term.IsTerminal(inFd) } } if !isTerminal { return fn() } var state *term.State var err error if t.Raw { state, err = term.MakeRaw(inFd) } else { state, err = term.SaveState(inFd) } if err != nil { return err } defer func() { if err := term.RestoreTerminal(inFd, state); err != nil { logrus.Errorf("Error resetting terminal: %v", err) } }() return fn() } release-utils-0.8.5/env/000077500000000000000000000000001467053121000150715ustar00rootroot00000000000000release-utils-0.8.5/env/env.go000066400000000000000000000020131467053121000162040ustar00rootroot00000000000000/* Copyright 2019 The Kubernetes Authors. 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 env import ( "sigs.k8s.io/release-utils/env/internal" ) // Default returns either the provided environment variable for the given key // or the default value def if not set. func Default(key, def string) string { value, ok := internal.Impl.LookupEnv(key) if !ok || value == "" { return def } return value } // IsSet returns true if an environment variable is set. func IsSet(key string) bool { _, ok := internal.Impl.LookupEnv(key) return ok } release-utils-0.8.5/env/env_test.go000066400000000000000000000045751467053121000172620ustar00rootroot00000000000000/* Copyright 2019 The Kubernetes Authors. 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 env import ( "testing" "github.com/stretchr/testify/require" "sigs.k8s.io/release-utils/env/internal" "sigs.k8s.io/release-utils/env/internal/internalfakes" ) func TestDefault(t *testing.T) { for _, tc := range []struct { prepare func(*internalfakes.FakeImpl) defaultValue string expected string }{ { // default LookupEnvReturns empty string and false prepare: func(mock *internalfakes.FakeImpl) { mock.LookupEnvReturns("", false) }, defaultValue: "default", expected: "default", }, { // default LookupEnvReturns empty string and true prepare: func(mock *internalfakes.FakeImpl) { mock.LookupEnvReturns("", true) }, defaultValue: "default", expected: "default", }, { // default LookupEnvReturns string and false prepare: func(mock *internalfakes.FakeImpl) { mock.LookupEnvReturns("value", false) }, defaultValue: "default", expected: "default", }, { // value is set prepare: func(mock *internalfakes.FakeImpl) { mock.LookupEnvReturns("value", true) }, defaultValue: "default", expected: "value", }, } { mock := &internalfakes.FakeImpl{} tc.prepare(mock) internal.Impl = mock res := Default("key", tc.defaultValue) require.Equal(t, tc.expected, res) } } func TestIsSet(t *testing.T) { for _, tc := range []struct { prepare func(*internalfakes.FakeImpl) expected bool }{ { // LookupEnvReturns false prepare: func(mock *internalfakes.FakeImpl) { mock.LookupEnvReturns("", false) }, expected: false, }, { // LookupEnvReturns true prepare: func(mock *internalfakes.FakeImpl) { mock.LookupEnvReturns("", true) }, expected: true, }, } { mock := &internalfakes.FakeImpl{} tc.prepare(mock) internal.Impl = mock res := IsSet("key") require.Equal(t, tc.expected, res) } } release-utils-0.8.5/env/internal/000077500000000000000000000000001467053121000167055ustar00rootroot00000000000000release-utils-0.8.5/env/internal/impl.go000066400000000000000000000021331467053121000201740ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. 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 internal import "os" //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate //go:generate /usr/bin/env bash -c "cat ../../scripts/boilerplate/boilerplate.generatego.txt internalfakes/fake_impl.go > internalfakes/_fake_impl.go && mv internalfakes/_fake_impl.go internalfakes/fake_impl.go" //counterfeiter:generate . impl type impl interface { LookupEnv(key string) (string, bool) } type defImpl struct{} var Impl impl = &defImpl{} func (defImpl) LookupEnv(key string) (string, bool) { return os.LookupEnv(key) } release-utils-0.8.5/env/internal/internalfakes/000077500000000000000000000000001467053121000215335ustar00rootroot00000000000000release-utils-0.8.5/env/internal/internalfakes/fake_impl.go000066400000000000000000000067001467053121000240140ustar00rootroot00000000000000/* Copyright The Kubernetes Authors. 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. */ // Code generated by counterfeiter. DO NOT EDIT. package internalfakes import ( "sync" ) type FakeImpl struct { LookupEnvStub func(string) (string, bool) lookupEnvMutex sync.RWMutex lookupEnvArgsForCall []struct { arg1 string } lookupEnvReturns struct { result1 string result2 bool } lookupEnvReturnsOnCall map[int]struct { result1 string result2 bool } invocations map[string][][]interface{} invocationsMutex sync.RWMutex } func (fake *FakeImpl) LookupEnv(arg1 string) (string, bool) { fake.lookupEnvMutex.Lock() ret, specificReturn := fake.lookupEnvReturnsOnCall[len(fake.lookupEnvArgsForCall)] fake.lookupEnvArgsForCall = append(fake.lookupEnvArgsForCall, struct { arg1 string }{arg1}) stub := fake.LookupEnvStub fakeReturns := fake.lookupEnvReturns fake.recordInvocation("LookupEnv", []interface{}{arg1}) fake.lookupEnvMutex.Unlock() if stub != nil { return stub(arg1) } if specificReturn { return ret.result1, ret.result2 } return fakeReturns.result1, fakeReturns.result2 } func (fake *FakeImpl) LookupEnvCallCount() int { fake.lookupEnvMutex.RLock() defer fake.lookupEnvMutex.RUnlock() return len(fake.lookupEnvArgsForCall) } func (fake *FakeImpl) LookupEnvCalls(stub func(string) (string, bool)) { fake.lookupEnvMutex.Lock() defer fake.lookupEnvMutex.Unlock() fake.LookupEnvStub = stub } func (fake *FakeImpl) LookupEnvArgsForCall(i int) string { fake.lookupEnvMutex.RLock() defer fake.lookupEnvMutex.RUnlock() argsForCall := fake.lookupEnvArgsForCall[i] return argsForCall.arg1 } func (fake *FakeImpl) LookupEnvReturns(result1 string, result2 bool) { fake.lookupEnvMutex.Lock() defer fake.lookupEnvMutex.Unlock() fake.LookupEnvStub = nil fake.lookupEnvReturns = struct { result1 string result2 bool }{result1, result2} } func (fake *FakeImpl) LookupEnvReturnsOnCall(i int, result1 string, result2 bool) { fake.lookupEnvMutex.Lock() defer fake.lookupEnvMutex.Unlock() fake.LookupEnvStub = nil if fake.lookupEnvReturnsOnCall == nil { fake.lookupEnvReturnsOnCall = make(map[int]struct { result1 string result2 bool }) } fake.lookupEnvReturnsOnCall[i] = struct { result1 string result2 bool }{result1, result2} } func (fake *FakeImpl) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() fake.lookupEnvMutex.RLock() defer fake.lookupEnvMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} for key, value := range fake.invocations { copiedInvocations[key] = value } return copiedInvocations } func (fake *FakeImpl) recordInvocation(key string, args []interface{}) { fake.invocationsMutex.Lock() defer fake.invocationsMutex.Unlock() if fake.invocations == nil { fake.invocations = map[string][][]interface{}{} } if fake.invocations[key] == nil { fake.invocations[key] = [][]interface{}{} } fake.invocations[key] = append(fake.invocations[key], args) } release-utils-0.8.5/go.mod000066400000000000000000000032131467053121000154060ustar00rootroot00000000000000module sigs.k8s.io/release-utils go 1.23 require ( github.com/blang/semver/v4 v4.0.0 github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1 github.com/moby/term v0.5.0 github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 github.com/uwu-tools/magex v0.10.0 k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 ) require ( github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.17.8 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/magefile/mage v1.15.0 // indirect github.com/mholt/archiver/v3 v3.5.1 // indirect github.com/nwaples/rardecode v1.1.3 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/ulikunitz/xz v0.5.12 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.15.0 // indirect golang.org/x/tools v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) release-utils-0.8.5/go.sum000066400000000000000000000227651467053121000154500ustar00rootroot00000000000000github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= 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/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1 h1:NicmruxkeqHjDv03SfSxqmaLuisddudfP3h5wdXFbhM= github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1/go.mod h1:eyp4DdUJAKkr9tvxR3jWhw2mDK7CWABMG5r9uyaKC7I= github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 h1:Up6+btDp321ZG5/zdSLo48H9Iaq0UQGthrhWC6pCxzE= github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481/go.mod h1:yKZQO8QE2bHlgozqWDiRVqTFlLQSj30K/6SAK8EeYFw= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc= github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/uwu-tools/magex v0.10.0 h1:eDDHw9izUPXEKXejY26VCtTK4LjuDoGkyWpgGscFO80= github.com/uwu-tools/magex v0.10.0/go.mod h1:TrSEhrL1xHfJVy6n05AUwFdcQndgwrbgL5ybPNKWmVY= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/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= k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCIXHaathvJg1C3ak= k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= release-utils-0.8.5/hash/000077500000000000000000000000001467053121000152245ustar00rootroot00000000000000release-utils-0.8.5/hash/hash.go000066400000000000000000000036471467053121000165100ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. 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 hash import ( "crypto/sha1" //nolint: gosec "crypto/sha256" "crypto/sha512" "encoding/hex" "errors" "fmt" "hash" "io" "os" "github.com/sirupsen/logrus" ) // SHA512ForFile returns the hex-encoded sha512 hash for the provided filename. func SHA512ForFile(filename string) (string, error) { return ForFile(filename, sha512.New()) } // SHA256ForFile returns the hex-encoded sha256 hash for the provided filename. func SHA256ForFile(filename string) (string, error) { return ForFile(filename, sha256.New()) } // SHA1ForFile returns the hex-encoded sha1 hash for the provided filename. // TODO: check if we can remove this function. func SHA1ForFile(filename string) (string, error) { return ForFile(filename, sha1.New()) //nolint: gosec } // ForFile returns the hex-encoded hash for the provided filename and hasher. func ForFile(filename string, hasher hash.Hash) (string, error) { if hasher == nil { return "", errors.New("provided hasher is nil") } f, err := os.Open(filename) if err != nil { return "", fmt.Errorf("open file %s: %w", filename, err) } defer func() { if err := f.Close(); err != nil { logrus.Warnf("Unable to close file %q: %v", filename, err) } }() hasher.Reset() if _, err := io.Copy(hasher, f); err != nil { return "", fmt.Errorf("hash file %s: %w", filename, err) } return hex.EncodeToString(hasher.Sum(nil)), nil } release-utils-0.8.5/hash/hash_test.go000066400000000000000000000077071467053121000175500ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. 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 hash_test import ( "crypto/sha1" //nolint: gosec "crypto/sha256" "hash" "os" "testing" "github.com/stretchr/testify/require" kHash "sigs.k8s.io/release-utils/hash" ) func TestSHA512ForFile(t *testing.T) { for _, tc := range []struct { prepare func() string expected string shouldError bool }{ { // success prepare: func() string { f, err := os.CreateTemp("", "") require.NoError(t, err) _, err = f.WriteString("test") require.NoError(t, err) return f.Name() }, expected: "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f88" + "19a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc" + "5fa9ad8e6f57f50028a8ff", shouldError: false, }, { // error open file prepare: func() string { return "" }, shouldError: true, }, } { filename := tc.prepare() res, err := kHash.SHA512ForFile(filename) if tc.shouldError { require.Error(t, err) } else { require.NoError(t, err) require.Equal(t, tc.expected, res) } } } func TestSHA256ForFile(t *testing.T) { for _, tc := range []struct { prepare func() string expected string shouldError bool }{ { // success prepare: func() string { f, err := os.CreateTemp("", "") require.NoError(t, err) _, err = f.WriteString("test") require.NoError(t, err) return f.Name() }, expected: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", shouldError: false, }, { // error open file prepare: func() string { return "" }, shouldError: true, }, } { filename := tc.prepare() res, err := kHash.SHA256ForFile(filename) if tc.shouldError { require.Error(t, err) } else { require.NoError(t, err) require.Equal(t, tc.expected, res) } } } func TestSHA1ForFile(t *testing.T) { for _, tc := range []struct { prepare func() string expected string shouldError bool }{ { // success prepare: func() string { f, err := os.CreateTemp("", "") require.NoError(t, err) _, err = f.WriteString("test") require.NoError(t, err) return f.Name() }, expected: "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", shouldError: false, }, { // error open file prepare: func() string { return "" }, shouldError: true, }, } { filename := tc.prepare() res, err := kHash.SHA1ForFile(filename) if tc.shouldError { require.Error(t, err) } else { require.NoError(t, err) require.Equal(t, tc.expected, res) } } } func TestForFile(t *testing.T) { for _, tc := range []struct { prepare func() (string, hash.Hash) expected string shouldError bool }{ { // success prepare: func() (string, hash.Hash) { f, err := os.CreateTemp("", "") require.NoError(t, err) _, err = f.WriteString("test") require.NoError(t, err) return f.Name(), sha1.New() //nolint: gosec }, expected: "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", shouldError: false, }, { // error hasher is nil prepare: func() (string, hash.Hash) { return "", nil }, shouldError: true, }, { // error file does not exist is nil prepare: func() (string, hash.Hash) { return "", sha256.New() }, shouldError: true, }, } { filename, hasher := tc.prepare() res, err := kHash.ForFile(filename, hasher) if tc.shouldError { require.Error(t, err) } else { require.NoError(t, err) require.Equal(t, tc.expected, res) } } } release-utils-0.8.5/http/000077500000000000000000000000001467053121000152605ustar00rootroot00000000000000release-utils-0.8.5/http/agent.go000066400000000000000000000402011467053121000167020ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. 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 http import ( "bytes" "errors" "fmt" "io" "math" "net/http" "sync" "time" "github.com/nozzle/throttler" "github.com/sirupsen/logrus" ) const ( defaultPostContentType = "application/octet-stream" ) //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate //go:generate /usr/bin/env bash -c "cat ../scripts/boilerplate/boilerplate.generatego.txt httpfakes/fake_agent_implementation.go > httpfakes/_fake_agent_implementation.go && mv httpfakes/_fake_agent_implementation.go httpfakes/fake_agent_implementation.go" // Agent is an http agent. type Agent struct { options *agentOptions AgentImplementation } // AgentImplementation is the actual implementation of the http calls // //counterfeiter:generate . AgentImplementation type AgentImplementation interface { SendPostRequest(*http.Client, string, []byte, string) (*http.Response, error) SendGetRequest(*http.Client, string) (*http.Response, error) SendHeadRequest(*http.Client, string) (*http.Response, error) } type defaultAgentImplementation struct{} // agentOptions has the configurable bits of the agent. type agentOptions struct { FailOnHTTPError bool // Set to true to fail on HTTP Status > 299 Retries uint // Number of times to retry when errors happen Timeout time.Duration // Timeout when fetching URLs MaxWaitTime time.Duration // Max waiting time when backing off PostContentType string // Content type to send when posting data MaxParallel uint // Maximum number of parallel requests when requesting groups } // String returns a string representation of the options. func (ao *agentOptions) String() string { return fmt.Sprintf( "HTTP.Agent options: Timeout: %d - Retries: %d - FailOnHTTPError: %+v", ao.Timeout, ao.Retries, ao.FailOnHTTPError, ) } var defaultAgentOptions = &agentOptions{ FailOnHTTPError: true, Retries: 3, Timeout: 3 * time.Second, MaxWaitTime: 60 * time.Second, PostContentType: defaultPostContentType, MaxParallel: 5, } // NewAgent return a new agent with default options. func NewAgent() *Agent { return &Agent{ AgentImplementation: &defaultAgentImplementation{}, options: defaultAgentOptions, } } // SetImplementation sets the agent implementation. func (a *Agent) SetImplementation(impl AgentImplementation) { a.AgentImplementation = impl } // WithTimeout sets the agent timeout. func (a *Agent) WithTimeout(timeout time.Duration) *Agent { a.options.Timeout = timeout return a } // WithRetries sets the number of times we'll attempt to fetch the URL. func (a *Agent) WithRetries(retries uint) *Agent { a.options.Retries = retries return a } // WithFailOnHTTPError determines if the agent fails on HTTP errors (HTTP status not in 200s). func (a *Agent) WithFailOnHTTPError(flag bool) *Agent { a.options.FailOnHTTPError = flag return a } // WithMaxParallel controls how many requests we do when fetching groups. func (a *Agent) WithMaxParallel(workers int) *Agent { //nolint:gosec // integer overflow highly unlikely a.options.MaxParallel = uint(workers) return a } // Client return an net/http client preconfigured with the agent options. func (a *Agent) Client() *http.Client { return &http.Client{ Timeout: a.options.Timeout, } } // Get returns the body a a GET request. func (a *Agent) Get(url string) (content []byte, err error) { request, err := a.GetRequest(url) if err != nil { return nil, fmt.Errorf("getting GET request: %w", err) } defer request.Body.Close() return a.readResponseToByteArray(request) } // GetRequest sends a GET request to a URL and returns the request and response. func (a *Agent) GetRequest(url string) (response *http.Response, err error) { logrus.Debugf("Sending GET request to %s", url) var try uint for { response, err = a.AgentImplementation.SendGetRequest(a.Client(), url) try++ if err == nil || try >= a.options.Retries { return response, err } // Do exponential backoff... waitTime := math.Pow(2, float64(try)) // ... but wait no more than 1 min if waitTime > 60 { waitTime = a.options.MaxWaitTime.Seconds() } logrus.Errorf( "Error getting URL (will retry %d more times in %.0f secs): %s", a.options.Retries-try, waitTime, err.Error(), ) time.Sleep(time.Duration(waitTime) * time.Second) } } // Post returns the body of a POST request. func (a *Agent) Post(url string, postData []byte) (content []byte, err error) { response, err := a.PostRequest(url, postData) if err != nil { return nil, fmt.Errorf("getting post request: %w", err) } defer response.Body.Close() return a.readResponseToByteArray(response) } // PostRequest sends the postData in a POST request to a URL and returns the request object. func (a *Agent) PostRequest(url string, postData []byte) (response *http.Response, err error) { logrus.Debugf("Sending POST request to %s", url) var try uint for { response, err = a.AgentImplementation.SendPostRequest(a.Client(), url, postData, a.options.PostContentType) try++ if err == nil || try >= a.options.Retries { return response, err } // Do exponential backoff... waitTime := math.Pow(2, float64(try)) // ... but wait no more than 1 min if waitTime > 60 { waitTime = a.options.MaxWaitTime.Seconds() } logrus.Errorf( "Error getting URL (will retry %d more times in %.0f secs): %s", a.options.Retries-try, waitTime, err.Error(), ) time.Sleep(time.Duration(waitTime) * time.Second) } } // Head returns the body of a HEAD request. func (a *Agent) Head(url string) (content []byte, err error) { response, err := a.HeadRequest(url) if err != nil { return nil, fmt.Errorf("getting head request: %w", err) } defer response.Body.Close() return a.readResponseToByteArray(response) } // HeadRequest sends a HEAD request to a URL and returns the request and response. func (a *Agent) HeadRequest(url string) (response *http.Response, err error) { logrus.Debugf("Sending HEAD request to %s", url) var try uint for { response, err = a.AgentImplementation.SendHeadRequest(a.Client(), url) try++ if err == nil || try >= a.options.Retries { return response, err } // Do exponential backoff... waitTime := math.Pow(2, float64(try)) // ... but wait no more than 1 min if waitTime > 60 { waitTime = a.options.MaxWaitTime.Seconds() } logrus.Errorf( "Error getting URL (will retry %d more times in %.0f secs): %s", a.options.Retries-try, waitTime, err.Error(), ) time.Sleep(time.Duration(waitTime) * time.Second) } } // SendPostRequest sends the actual HTTP post to the server. func (impl *defaultAgentImplementation) SendPostRequest( client *http.Client, url string, postData []byte, contentType string, ) (response *http.Response, err error) { if contentType == "" { contentType = defaultPostContentType } response, err = client.Post(url, contentType, bytes.NewBuffer(postData)) if err != nil { return response, fmt.Errorf("posting data to %s: %w", url, err) } return response, nil } // SendGetRequest performs the actual request. func (impl *defaultAgentImplementation) SendGetRequest(client *http.Client, url string) ( response *http.Response, err error, ) { response, err = client.Get(url) if err != nil { return response, fmt.Errorf("getting %s: %w", url, err) } return response, nil } // SendHeadRequest performs the actual request. func (impl *defaultAgentImplementation) SendHeadRequest(client *http.Client, url string) ( response *http.Response, err error, ) { response, err = client.Head(url) if err != nil { return response, fmt.Errorf("sending head request %s: %w", url, err) } return response, nil } // readResponseToByteArray returns the contents of an http response as a byte array. func (a *Agent) readResponseToByteArray(response *http.Response) ([]byte, error) { var b bytes.Buffer if err := a.readResponse(response, &b); err != nil { return nil, fmt.Errorf("reading array buffer: %w", err) } return b.Bytes(), nil } // readResponse reads and interprets the response to an HTTP request to an io.Writer. // If the response status is < 200 or >= 300 and FailOnHTTPError is set, the function // will return an error. // // This function will close the response body reader. func (a *Agent) readResponse(response *http.Response, w io.Writer) (err error) { // Read the response body defer response.Body.Close() if _, err := io.Copy(w, response.Body); err != nil { return fmt.Errorf("reading response: %w", err) } // Check the https response code if response.StatusCode < 200 || response.StatusCode >= 300 { if a.options.FailOnHTTPError { return fmt.Errorf( "HTTP error %s for %s", response.Status, response.Request.URL, ) } logrus.Warnf("Got HTTP error but FailOnHTTPError not set: %s", response.Status) } return err } // GetToWriter sends a get request and writes the response to an io.Writer. func (a *Agent) GetToWriter(w io.Writer, url string) error { resp, err := a.AgentImplementation.SendGetRequest(a.Client(), url) if err != nil { return fmt.Errorf("sending GET request: %w", err) } return a.readResponse(resp, w) } // PostToWriter sends a request to a url and writes the response to an io.Writer. func (a *Agent) PostToWriter(w io.Writer, url string, postData []byte) error { resp, err := a.AgentImplementation.SendPostRequest(a.Client(), url, postData, a.options.PostContentType) if err != nil { return fmt.Errorf("sending POST request: %w", err) } return a.readResponse(resp, w) } // GetRequestGroup behaves like agent.SendGetRequest() but takes a group of URLs // and performs the requests in parallel. The number of simultaneous requests is // controlled by options.MaxParallel. func (a *Agent) GetRequestGroup(urls []string) ([]*http.Response, []error) { //nolint:gosec // integer overflow highly unlikely t := throttler.New(int(a.options.MaxParallel), len(urls)) ret := make([]*http.Response, len(urls)) errs := make([]error, len(urls)) m := sync.Mutex{} for i := range urls { go func(url string) { //nolint: bodyclose // We don't close here as we're returning the response resp, err := a.AgentImplementation.SendGetRequest(a.Client(), url) m.Lock() ret[i] = resp errs[i] = err m.Unlock() t.Done(err) }(urls[i]) t.Throttle() } return ret, errs } // PostRequestGroup behaves like agent.Post() but takes a group of URLs and performs the // requests in parallel. The number of simultaneous requests is controlled by // options.MaxParallel. // // The list of URLs and postData byte arrays are required to be of equal length. // If postData has less elements than the URL list, the function will exit early, // failing all requests. func (a *Agent) PostRequestGroup(urls []string, postData [][]byte) ([]*http.Response, []error) { ret := make([]*http.Response, len(urls)) errs := make([]error, len(urls)) // URLs and postData arrays must be equal in length. If not exit now. if len(postData) != len(urls) { err := errors.New("unable to perform requests, same number URLs and POST payloads required") for i := range urls { errs[i] = err } return ret, errs } //nolint:gosec // integer overflow highly unlikely t := throttler.New(int(a.options.MaxParallel), len(urls)) m := sync.Mutex{} for i := range urls { go func(url string, pdata []byte) { //nolint: bodyclose // We don't close here as we're returning the raw response resp, err := a.AgentImplementation.SendPostRequest( a.Client(), url, pdata, a.options.PostContentType, ) m.Lock() ret[i] = resp errs[i] = err m.Unlock() t.Done(err) }(urls[i], postData[i]) t.Throttle() } return ret, errs } // PostGroup behaves just as Post() but takes a group of URLs and performs // the requests in parallel. The number of simultaneous requests is controlled by // options.MaxParallel. // // The list of URLs and postData byte arrays are expected to be of equal length. // If postData has less elements than the url list, those urls without a corresponding // postData array will return an error. func (a *Agent) PostGroup(urls []string, postData [][]byte) ([][]byte, []error) { //nolint: bodyclose // Next line closes them resps, errs := a.PostRequestGroup(urls, postData) defer closeHTTPResponseGroup(resps) c := make([][]byte, len(urls)) for i, r := range resps { if r != nil { d, err := a.readResponseToByteArray(r) if err != nil { errs[i] = fmt.Errorf("reading group response #%d: %w", i, err) continue } c[i] = d } } return c, errs } // closeHTTPResponseGroup is an internal func that closes the response bodies. func closeHTTPResponseGroup(resps []*http.Response) { for i := range resps { if resps[i] == nil { continue } resps[i].Body.Close() } } // PostToWriterGroup behaves just as PostToWriter() but takes a group of URLs // and performs the requests in parallel. The number of simultaneous requests // is controlled by options.MaxParallel. // // The list of URLs and postData byte arrays are expected to be of equal length. // If postData has less elements than the url list, those urls without a corresponding // postData array will return an error. // // If the w writers slice contains a single writer, all the responses will be // written to the single writer. If the writers array contains more than one // io.Writer, each request will be written to its corresponding writer unless it // is missing, in that case the request will return an an error. The requests are // guaranteed to go into the writer in order. func (a *Agent) PostToWriterGroup(w []io.Writer, urls []string, postData [][]byte) []error { //nolint: bodyclose // Next line closes them resps, errs := a.PostRequestGroup(urls, postData) defer closeHTTPResponseGroup(resps) for i, r := range resps { if r == nil { continue } var err error if len(w) == 1 { err = a.readResponse(r, w[0]) } else { if i >= len(w) { err = fmt.Errorf("request %d has no writer defined", i) } else { err = a.readResponse(r, w[i]) } } if err != nil { errs[i] = fmt.Errorf("writing group response #%d: %w", i, err) continue } } return errs } // GetGroup behaves just as Get() but takes a group of URLs and performs // the requests in parallel. The number of simultaneous requests is controlled by // options.MaxParallel. func (a *Agent) GetGroup(urls []string) ([][]byte, []error) { //nolint: bodyclose // Next line closes them resps, errs := a.GetRequestGroup(urls) defer closeHTTPResponseGroup(resps) c := make([][]byte, len(urls)) for i, r := range resps { if r != nil { d, err := a.readResponseToByteArray(r) if err != nil { errs[i] = fmt.Errorf("reading group response #%d: %w", i, err) continue } c[i] = d } } return c, errs } // GetToWriterGroup behaves just as GetToWriter() but takes a group of URLs // and performs the requests in parallel. The number of simultaneous requests // is controlled by options.MaxParallel. // // If the w writers slice contains a single writer, all the responses will be // written to the single writer. If the writers array contains more than one // io.Writer, each request will be written to its corresponding writer unless it // is missing in which case the request will return an an error. The requests are // guaranteed to go into the writer in order. func (a *Agent) GetToWriterGroup(w []io.Writer, urls []string) []error { //nolint: bodyclose resps, errs := a.GetRequestGroup(urls) defer closeHTTPResponseGroup(resps) for i, r := range resps { if r == nil { continue } var err error if len(w) == 1 { err = a.readResponse(r, w[0]) } else { if i >= len(w) { err = fmt.Errorf("request %d has no writer defined", i) } else { err = a.readResponse(r, w[i]) } } if err != nil { errs[i] = fmt.Errorf("writing group response #%d: %w", i, err) continue } } return errs } release-utils-0.8.5/http/doc.go000066400000000000000000000055561467053121000163670ustar00rootroot00000000000000/* Copyright 2024 The Kubernetes Authors. 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 http provides a configurable agent to talk to http servers. # Function Families It provides three families of functions for the GET, POST and HEAD methods that return the raw http.Response, the response contents as a byte slice or to write the response to a writer. Each of these functions also provide a _Group_ equivalent that takes a list of URLs and performs the requests in parallel. The easiest way to understand the functions is this expression: METHOD[Request|ToWriter][Group] So, for examaple, the functions for the POST method include the following variations, note that the Group variants take and return the same arguments but in plural form, ie same type but a slice: Post(string url, []byte postData) ([]byte, error) PostRequest(string url, []byte postData) (*http.Response, error) PostToWriter(io.Writer w, string url, []byte postData) error PostGroup([]string urls, [][]byte postData) ([][]byte, []error) PostRequestGroup([]string urls, [][]byte postData) ([]*http.Response, []error) PostToWriterGroup([]io.Writer w, []string urls, [][]byte postData) []error # Group Requests All the _Group_ families perform the requests in parallel. The number of simultaneous requests can be controlled with the .WithMaxParallel(int) option: # Create an HTTP agent that performs two requests at a time: agent := http.NewAgent().WithMaxParallel(2) All group requests take arguments in slices and return data and errors in slices guaranteed to be of the same length and order as the arguments. To check the returned error slice for success in a single shot the errors.Join() function comes in handy: responses, errs := agent.GetGroup(urlList) if errors.Join(errs) != nil { // Handle errors here } # Single and Multiple Writer Output The ToWriterGroup variants take a list of writers in their first arguments. Usually, the data returned by the requests will be written to each corresponding writer in the slice (eg request #5 to writer #5). There is an exception though, if the writer slice contains a single writer, the data from all requests will be written - in order - into the single writer. This allows for simple piping to a single output sink (ie all output to STDOUT). # Example The following example shows a code snippet that fetches ten photographs in parallel and writes them to disk. */ package http release-utils-0.8.5/http/example_multi_test.go000066400000000000000000000036351467053121000215220ustar00rootroot00000000000000/* Copyright 2024 The Kubernetes Authors. 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 http_test import ( "errors" "fmt" "io" "os" "github.com/sirupsen/logrus" "sigs.k8s.io/release-utils/http" ) func Example() { // This example fetches 10 photographs from flick in parallel agent := http.NewAgent() w := []io.Writer{} urls := []string{ "https://live.staticflickr.com/65535/53863838503_3490725fab.jpg", "https://live.staticflickr.com/65535/53862224352_a9949bb818.jpg", "https://live.staticflickr.com/65535/53863076331_570818d62f_w.jpg", "https://live.staticflickr.com/65535/53863751331_aa8cc7c233_w.jpg", "https://live.staticflickr.com/65535/53862636262_3ec860a652.jpg", "https://live.staticflickr.com/65535/53863034561_079ea0a87b_z.jpg", "https://live.staticflickr.com/65535/53862940596_5a991b2271_w.jpg", "https://live.staticflickr.com/65535/53863423169_90f8e13b7f_z", "https://live.staticflickr.com/65535/53863136849_965bd39df1_n.jpg", "https://live.staticflickr.com/65535/53863672556_1050bbf01b_n.jpg", } for i := range urls { f, err := os.Create(fmt.Sprintf("/tmp/photo-%d.jpg", i)) if err != nil { logrus.Fatal("error opening file") } w = append(w, f) } defer func() { for i := range w { w[i].(*os.File).Close() } }() errs := agent.GetToWriterGroup(w, urls) if errors.Join(errs...) != nil { logrus.Fatalf("%d errors fetching photos: %v", len(errs), errors.Join(errs...)) } // output: } release-utils-0.8.5/http/http.go000066400000000000000000000020141467053121000165630ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. 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 http import ( "bytes" ) // GetURLResponse performs a get request and returns the response contents as a // string if successful. // // Deprecated: Use http.Agent.Get() instead. This function will be removed in a // future version of this package. func GetURLResponse(url string, trim bool) (string, error) { resp, err := NewAgent().Get(url) if err != nil { return "", err } if trim { resp = bytes.TrimSpace(resp) } return string(resp), nil } release-utils-0.8.5/http/http_test.go000066400000000000000000000304061467053121000176300ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. 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 http_test import ( "bytes" "errors" "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/stretchr/testify/require" khttp "sigs.k8s.io/release-utils/http" "sigs.k8s.io/release-utils/http/httpfakes" ) func TestGetURLResponseSuccess(t *testing.T) { // Given server := httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, _ *http.Request) { _, err := io.WriteString(w, "") if err != nil { t.Fail() } })) defer server.Close() // When actual, err := khttp.GetURLResponse(server.URL, false) // Then require.NoError(t, err) require.Empty(t, actual) } func TestGetURLResponseSuccessTrimmed(t *testing.T) { // Given const expected = " some test " server := httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, _ *http.Request) { _, err := io.WriteString(w, expected) if err != nil { t.Fail() } })) defer server.Close() // When actual, err := khttp.GetURLResponse(server.URL, true) // Then require.NoError(t, err) require.Equal(t, strings.TrimSpace(expected), actual) } func TestGetURLResponseFailedStatus(t *testing.T) { // Given server := httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadRequest) })) defer server.Close() // When _, err := khttp.GetURLResponse(server.URL, true) // Then require.Error(t, err) } func NewTestAgent() *khttp.Agent { agent := khttp.NewAgent() agent.SetImplementation(&httpfakes.FakeAgentImplementation{}) return agent } func TestAgentPost(t *testing.T) { t.Parallel() agent := NewTestAgent().WithRetries(0) resp := getTestResponse() defer resp.Body.Close() // First simulate a successful request fake := &httpfakes.FakeAgentImplementation{} fake.SendPostRequestReturns(resp, nil) agent.SetImplementation(fake) body, err := agent.Post("http://www.example.com/", []byte("Test string")) require.NoError(t, err) require.Equal(t, body, []byte("hello sig-release!")) // Now check error is handled fake.SendPostRequestReturns(resp, errors.New("HTTP Post error")) agent.SetImplementation(fake) _, err = agent.Post("http://www.example.com/", []byte("Test string")) require.Error(t, err) } func TestAgentGet(t *testing.T) { t.Parallel() agent := NewTestAgent().WithRetries(0) for _, tc := range []struct { name string mustErr bool expected []byte prepare func(*httpfakes.FakeAgentImplementation) }{ { "no-error", false, []byte("hello sig-release!"), func(fai *httpfakes.FakeAgentImplementation) { t.Helper() resp := getTestResponse() defer resp.Body.Close() fai.SendGetRequestReturns(resp, nil) }, }, { "error", true, nil, func(fai *httpfakes.FakeAgentImplementation) { t.Helper() fai.SendGetRequestReturns(nil, errors.New("HTTP Post error")) }, }, } { t.Run(tc.name, func(t *testing.T) { t.Parallel() fake := &httpfakes.FakeAgentImplementation{} tc.prepare(fake) agent.SetImplementation(fake) b, err := agent.Get("http://www.example.com/") if tc.mustErr { require.Error(t, err) return } require.NoError(t, err) require.Equal(t, tc.expected, b) }) } } func TestAgentGetToWriter(t *testing.T) { agent := NewTestAgent() for _, tc := range []struct { n string prepare func(*httpfakes.FakeAgentImplementation, *http.Response) mustErr bool }{ { n: "success", prepare: func(fake *httpfakes.FakeAgentImplementation, resp *http.Response) { fake.SendGetRequestReturns(resp, nil) }, }, { n: "fail", prepare: func(fake *httpfakes.FakeAgentImplementation, resp *http.Response) { fake.SendGetRequestReturns(resp, errors.New("HTTP Post error")) }, mustErr: true, }, } { t.Run(tc.n, func(t *testing.T) { // First simulate a successful request fake := &httpfakes.FakeAgentImplementation{} resp := getTestResponse() defer resp.Body.Close() tc.prepare(fake, resp) var buf bytes.Buffer agent.SetImplementation(fake) err := agent.GetToWriter(&buf, "http://www.example.com/") if tc.mustErr { require.Error(t, err) return } require.NoError(t, err) require.Equal(t, buf.Bytes(), []byte("hello sig-release!")) }) } } func TestAgentHead(t *testing.T) { t.Parallel() agent := NewTestAgent().WithRetries(0) resp := getTestResponse() defer resp.Body.Close() // First simulate a successful request fake := &httpfakes.FakeAgentImplementation{} fake.SendHeadRequestReturns(resp, nil) agent.SetImplementation(fake) b, err := agent.Head("http://www.example.com/") require.NoError(t, err) require.Equal(t, b, []byte("hello sig-release!")) // Now check error is handled fake.SendHeadRequestReturns(resp, errors.New("HTTP Head error")) agent.SetImplementation(fake) _, err = agent.Head("http://www.example.com/") require.Error(t, err) } func getTestResponse() *http.Response { return &http.Response{ Status: "200 OK", StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("hello sig-release!"))), ContentLength: 18, Close: true, Request: &http.Request{}, } } func TestAgentPostToWriter(t *testing.T) { for _, tc := range []struct { n string prepare func(*httpfakes.FakeAgentImplementation, *http.Response) mustErr bool }{ { n: "success", prepare: func(fake *httpfakes.FakeAgentImplementation, resp *http.Response) { fake.SendPostRequestReturns(resp, nil) }, }, { n: "fail", prepare: func(fake *httpfakes.FakeAgentImplementation, resp *http.Response) { fake.SendPostRequestReturns(resp, errors.New("HTTP Post error")) }, mustErr: true, }, } { t.Run(tc.n, func(t *testing.T) { agent := NewTestAgent() // First simulate a successful request fake := &httpfakes.FakeAgentImplementation{} resp := getTestResponse() defer resp.Body.Close() tc.prepare(fake, resp) var buf bytes.Buffer agent.SetImplementation(fake) err := agent.PostToWriter(&buf, "http://www.example.com/", []byte{}) if tc.mustErr { require.Error(t, err) return } require.NoError(t, err) require.Equal(t, buf.Bytes(), []byte("hello sig-release!")) }) } } func TestAgentOptions(t *testing.T) { agent := NewTestAgent() fake := &httpfakes.FakeAgentImplementation{} resp := &http.Response{ Status: "Fake not found", StatusCode: http.StatusNotFound, Body: io.NopCloser(bytes.NewReader([]byte("hello sig-release!"))), ContentLength: 18, Close: true, Request: &http.Request{}, } defer resp.Body.Close() fake.SendGetRequestReturns(resp, nil) agent.SetImplementation(fake) // Test FailOnHTTPError // First we fail on server errors _, err := agent.WithFailOnHTTPError(true).Get("http://example.com/") require.Error(t, err) // Then we just note them and do not fail _, err = agent.WithFailOnHTTPError(false).Get("http://example.com/") require.NoError(t, err) } // closeHTTPResponseGroup is an internal func that closes the response bodies. func closeHTTPResponseGroup(resps []*http.Response) { for i := range resps { if resps[i] == nil { continue } resps[i].Body.Close() } } func TestAgentGroupGetRequest(t *testing.T) { fake := &httpfakes.FakeAgentImplementation{} fakeUrls := []string{"http://www/1", "http://www/2", "http://www/3"} fake.SendGetRequestCalls(func(_ *http.Client, s string) (*http.Response, error) { switch s { case fakeUrls[0]: return &http.Response{ Status: "Fake OK", StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("hello sig-release!"))), ContentLength: 18, Close: true, Request: &http.Request{}, }, nil case fakeUrls[1]: return &http.Response{ Status: "Fake not found", StatusCode: http.StatusNotFound, Body: io.NopCloser(bytes.NewReader([]byte("hello sig-release!"))), ContentLength: 18, Close: true, Request: &http.Request{}, }, nil case fakeUrls[2]: return nil, errors.New("malformed url") } return nil, nil }) for _, tc := range []struct { name string workers int }{ {"no-parallelism", 1}, {"one-per-request", 3}, {"spare-workers", 5}, } { t.Run(tc.name, func(t *testing.T) { // No retries as the errors are synthetic agent := NewTestAgent().WithRetries(0).WithFailOnHTTPError(false).WithMaxParallel(tc.workers) agent.SetImplementation(fake) //nolint: bodyclose // The next line closes them resps, errs := agent.GetRequestGroup(fakeUrls) defer closeHTTPResponseGroup(resps) require.Len(t, resps, 3) require.Len(t, errs, 3) require.NoError(t, errs[0]) require.NoError(t, errs[1]) require.Error(t, errs[2]) require.Equal(t, http.StatusOK, resps[0].StatusCode) require.Equal(t, http.StatusNotFound, resps[1].StatusCode) require.Nil(t, resps[2]) }) } } func TestAgentPostRequestGroup(t *testing.T) { t.Parallel() fake := &httpfakes.FakeAgentImplementation{} errorURL := "fake:error" httpErrorURL := "fake:httpError" noErrorURL := "fake:ok" fake.SendPostRequestCalls(func(_ *http.Client, s string, _ []byte, _ string) (*http.Response, error) { switch s { case noErrorURL: return &http.Response{ Status: "Fake OK", StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("hello sig-release!"))), ContentLength: 18, Close: true, Request: &http.Request{}, }, nil case httpErrorURL: return &http.Response{ Status: "Fake not found", StatusCode: http.StatusNotFound, Body: io.NopCloser(bytes.NewReader([]byte("hello sig-release!"))), ContentLength: 18, Close: true, Request: &http.Request{}, }, fmt.Errorf("HTTP error %d for %s", http.StatusNotFound, s) case errorURL: return nil, errors.New("malformed url") } return nil, nil }) for _, tc := range []struct { name string workers int mustErr bool urls []string postData [][]byte }{ {"no-parallelism", 1, false, []string{noErrorURL, noErrorURL, noErrorURL}, make([][]byte, 3)}, {"one-per-request", 3, false, []string{noErrorURL, noErrorURL, noErrorURL}, make([][]byte, 3)}, {"spare-workers", 5, false, []string{noErrorURL, noErrorURL, noErrorURL}, make([][]byte, 3)}, {"uneven-postdata", 5, true, []string{noErrorURL, noErrorURL, noErrorURL}, make([][]byte, 2)}, {"uneven-postdata2", 5, true, []string{noErrorURL, noErrorURL, noErrorURL}, make([][]byte, 4)}, {"http-error", 5, true, []string{noErrorURL, httpErrorURL, noErrorURL}, make([][]byte, 3)}, {"software-error", 5, true, []string{noErrorURL, errorURL, noErrorURL}, make([][]byte, 3)}, } { t.Run(tc.name, func(t *testing.T) { t.Parallel() // No retries as the errors are synthetic agent := NewTestAgent().WithRetries(0).WithFailOnHTTPError(false).WithMaxParallel(tc.workers) agent.SetImplementation(fake) //nolint: bodyclose resps, errs := agent.PostRequestGroup(tc.urls, tc.postData) closeHTTPResponseGroup(resps) // If urls and postdata don't all errors should be errors if len(tc.urls) != len(tc.postData) { for i := range errs { require.Error(t, errs[i]) } return } // Check for at least on error if tc.mustErr { require.Error(t, errors.Join(errs...)) } else { require.NoError(t, errors.Join(errs...)) } require.Len(t, resps, len(tc.urls)) require.Len(t, errs, len(tc.urls)) for i := range tc.urls { switch tc.urls[i] { case noErrorURL: require.NoError(t, errs[i]) require.NotNil(t, resps[i]) require.Equal(t, http.StatusOK, resps[i].StatusCode) case httpErrorURL: require.Error(t, errs[i]) require.NotNil(t, resps[i]) require.Equal(t, http.StatusNotFound, resps[i].StatusCode) case errorURL: require.Error(t, errs[i]) } } }) } } release-utils-0.8.5/http/httpfakes/000077500000000000000000000000001467053121000172515ustar00rootroot00000000000000release-utils-0.8.5/http/httpfakes/fake_agent_implementation.go000066400000000000000000000232331467053121000247740ustar00rootroot00000000000000/* Copyright The Kubernetes Authors. 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. */ // Code generated by counterfeiter. DO NOT EDIT. package httpfakes import ( httpa "net/http" "sync" "sigs.k8s.io/release-utils/http" ) type FakeAgentImplementation struct { SendGetRequestStub func(*httpa.Client, string) (*httpa.Response, error) sendGetRequestMutex sync.RWMutex sendGetRequestArgsForCall []struct { arg1 *httpa.Client arg2 string } sendGetRequestReturns struct { result1 *httpa.Response result2 error } sendGetRequestReturnsOnCall map[int]struct { result1 *httpa.Response result2 error } SendHeadRequestStub func(*httpa.Client, string) (*httpa.Response, error) sendHeadRequestMutex sync.RWMutex sendHeadRequestArgsForCall []struct { arg1 *httpa.Client arg2 string } sendHeadRequestReturns struct { result1 *httpa.Response result2 error } sendHeadRequestReturnsOnCall map[int]struct { result1 *httpa.Response result2 error } SendPostRequestStub func(*httpa.Client, string, []byte, string) (*httpa.Response, error) sendPostRequestMutex sync.RWMutex sendPostRequestArgsForCall []struct { arg1 *httpa.Client arg2 string arg3 []byte arg4 string } sendPostRequestReturns struct { result1 *httpa.Response result2 error } sendPostRequestReturnsOnCall map[int]struct { result1 *httpa.Response result2 error } invocations map[string][][]interface{} invocationsMutex sync.RWMutex } func (fake *FakeAgentImplementation) SendGetRequest(arg1 *httpa.Client, arg2 string) (*httpa.Response, error) { fake.sendGetRequestMutex.Lock() ret, specificReturn := fake.sendGetRequestReturnsOnCall[len(fake.sendGetRequestArgsForCall)] fake.sendGetRequestArgsForCall = append(fake.sendGetRequestArgsForCall, struct { arg1 *httpa.Client arg2 string }{arg1, arg2}) stub := fake.SendGetRequestStub fakeReturns := fake.sendGetRequestReturns fake.recordInvocation("SendGetRequest", []interface{}{arg1, arg2}) fake.sendGetRequestMutex.Unlock() if stub != nil { return stub(arg1, arg2) } if specificReturn { return ret.result1, ret.result2 } return fakeReturns.result1, fakeReturns.result2 } func (fake *FakeAgentImplementation) SendGetRequestCallCount() int { fake.sendGetRequestMutex.RLock() defer fake.sendGetRequestMutex.RUnlock() return len(fake.sendGetRequestArgsForCall) } func (fake *FakeAgentImplementation) SendGetRequestCalls(stub func(*httpa.Client, string) (*httpa.Response, error)) { fake.sendGetRequestMutex.Lock() defer fake.sendGetRequestMutex.Unlock() fake.SendGetRequestStub = stub } func (fake *FakeAgentImplementation) SendGetRequestArgsForCall(i int) (*httpa.Client, string) { fake.sendGetRequestMutex.RLock() defer fake.sendGetRequestMutex.RUnlock() argsForCall := fake.sendGetRequestArgsForCall[i] return argsForCall.arg1, argsForCall.arg2 } func (fake *FakeAgentImplementation) SendGetRequestReturns(result1 *httpa.Response, result2 error) { fake.sendGetRequestMutex.Lock() defer fake.sendGetRequestMutex.Unlock() fake.SendGetRequestStub = nil fake.sendGetRequestReturns = struct { result1 *httpa.Response result2 error }{result1, result2} } func (fake *FakeAgentImplementation) SendGetRequestReturnsOnCall(i int, result1 *httpa.Response, result2 error) { fake.sendGetRequestMutex.Lock() defer fake.sendGetRequestMutex.Unlock() fake.SendGetRequestStub = nil if fake.sendGetRequestReturnsOnCall == nil { fake.sendGetRequestReturnsOnCall = make(map[int]struct { result1 *httpa.Response result2 error }) } fake.sendGetRequestReturnsOnCall[i] = struct { result1 *httpa.Response result2 error }{result1, result2} } func (fake *FakeAgentImplementation) SendHeadRequest(arg1 *httpa.Client, arg2 string) (*httpa.Response, error) { fake.sendHeadRequestMutex.Lock() ret, specificReturn := fake.sendHeadRequestReturnsOnCall[len(fake.sendHeadRequestArgsForCall)] fake.sendHeadRequestArgsForCall = append(fake.sendHeadRequestArgsForCall, struct { arg1 *httpa.Client arg2 string }{arg1, arg2}) stub := fake.SendHeadRequestStub fakeReturns := fake.sendHeadRequestReturns fake.recordInvocation("SendHeadRequest", []interface{}{arg1, arg2}) fake.sendHeadRequestMutex.Unlock() if stub != nil { return stub(arg1, arg2) } if specificReturn { return ret.result1, ret.result2 } return fakeReturns.result1, fakeReturns.result2 } func (fake *FakeAgentImplementation) SendHeadRequestCallCount() int { fake.sendHeadRequestMutex.RLock() defer fake.sendHeadRequestMutex.RUnlock() return len(fake.sendHeadRequestArgsForCall) } func (fake *FakeAgentImplementation) SendHeadRequestCalls(stub func(*httpa.Client, string) (*httpa.Response, error)) { fake.sendHeadRequestMutex.Lock() defer fake.sendHeadRequestMutex.Unlock() fake.SendHeadRequestStub = stub } func (fake *FakeAgentImplementation) SendHeadRequestArgsForCall(i int) (*httpa.Client, string) { fake.sendHeadRequestMutex.RLock() defer fake.sendHeadRequestMutex.RUnlock() argsForCall := fake.sendHeadRequestArgsForCall[i] return argsForCall.arg1, argsForCall.arg2 } func (fake *FakeAgentImplementation) SendHeadRequestReturns(result1 *httpa.Response, result2 error) { fake.sendHeadRequestMutex.Lock() defer fake.sendHeadRequestMutex.Unlock() fake.SendHeadRequestStub = nil fake.sendHeadRequestReturns = struct { result1 *httpa.Response result2 error }{result1, result2} } func (fake *FakeAgentImplementation) SendHeadRequestReturnsOnCall(i int, result1 *httpa.Response, result2 error) { fake.sendHeadRequestMutex.Lock() defer fake.sendHeadRequestMutex.Unlock() fake.SendHeadRequestStub = nil if fake.sendHeadRequestReturnsOnCall == nil { fake.sendHeadRequestReturnsOnCall = make(map[int]struct { result1 *httpa.Response result2 error }) } fake.sendHeadRequestReturnsOnCall[i] = struct { result1 *httpa.Response result2 error }{result1, result2} } func (fake *FakeAgentImplementation) SendPostRequest(arg1 *httpa.Client, arg2 string, arg3 []byte, arg4 string) (*httpa.Response, error) { var arg3Copy []byte if arg3 != nil { arg3Copy = make([]byte, len(arg3)) copy(arg3Copy, arg3) } fake.sendPostRequestMutex.Lock() ret, specificReturn := fake.sendPostRequestReturnsOnCall[len(fake.sendPostRequestArgsForCall)] fake.sendPostRequestArgsForCall = append(fake.sendPostRequestArgsForCall, struct { arg1 *httpa.Client arg2 string arg3 []byte arg4 string }{arg1, arg2, arg3Copy, arg4}) stub := fake.SendPostRequestStub fakeReturns := fake.sendPostRequestReturns fake.recordInvocation("SendPostRequest", []interface{}{arg1, arg2, arg3Copy, arg4}) fake.sendPostRequestMutex.Unlock() if stub != nil { return stub(arg1, arg2, arg3, arg4) } if specificReturn { return ret.result1, ret.result2 } return fakeReturns.result1, fakeReturns.result2 } func (fake *FakeAgentImplementation) SendPostRequestCallCount() int { fake.sendPostRequestMutex.RLock() defer fake.sendPostRequestMutex.RUnlock() return len(fake.sendPostRequestArgsForCall) } func (fake *FakeAgentImplementation) SendPostRequestCalls(stub func(*httpa.Client, string, []byte, string) (*httpa.Response, error)) { fake.sendPostRequestMutex.Lock() defer fake.sendPostRequestMutex.Unlock() fake.SendPostRequestStub = stub } func (fake *FakeAgentImplementation) SendPostRequestArgsForCall(i int) (*httpa.Client, string, []byte, string) { fake.sendPostRequestMutex.RLock() defer fake.sendPostRequestMutex.RUnlock() argsForCall := fake.sendPostRequestArgsForCall[i] return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 } func (fake *FakeAgentImplementation) SendPostRequestReturns(result1 *httpa.Response, result2 error) { fake.sendPostRequestMutex.Lock() defer fake.sendPostRequestMutex.Unlock() fake.SendPostRequestStub = nil fake.sendPostRequestReturns = struct { result1 *httpa.Response result2 error }{result1, result2} } func (fake *FakeAgentImplementation) SendPostRequestReturnsOnCall(i int, result1 *httpa.Response, result2 error) { fake.sendPostRequestMutex.Lock() defer fake.sendPostRequestMutex.Unlock() fake.SendPostRequestStub = nil if fake.sendPostRequestReturnsOnCall == nil { fake.sendPostRequestReturnsOnCall = make(map[int]struct { result1 *httpa.Response result2 error }) } fake.sendPostRequestReturnsOnCall[i] = struct { result1 *httpa.Response result2 error }{result1, result2} } func (fake *FakeAgentImplementation) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() fake.sendGetRequestMutex.RLock() defer fake.sendGetRequestMutex.RUnlock() fake.sendHeadRequestMutex.RLock() defer fake.sendHeadRequestMutex.RUnlock() fake.sendPostRequestMutex.RLock() defer fake.sendPostRequestMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} for key, value := range fake.invocations { copiedInvocations[key] = value } return copiedInvocations } func (fake *FakeAgentImplementation) recordInvocation(key string, args []interface{}) { fake.invocationsMutex.Lock() defer fake.invocationsMutex.Unlock() if fake.invocations == nil { fake.invocations = map[string][][]interface{}{} } if fake.invocations[key] == nil { fake.invocations[key] = [][]interface{}{} } fake.invocations[key] = append(fake.invocations[key], args) } var _ http.AgentImplementation = new(FakeAgentImplementation) release-utils-0.8.5/internal/000077500000000000000000000000001467053121000161155ustar00rootroot00000000000000release-utils-0.8.5/internal/tools/000077500000000000000000000000001467053121000172555ustar00rootroot00000000000000release-utils-0.8.5/internal/tools/tools.go000066400000000000000000000014021467053121000207410ustar00rootroot00000000000000// +build tools /* Copyright 2021 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // This is used to import things required by build scripts, to force `go mod` to see them as dependencies package internal import ( _ "github.com/maxbrunsfeld/counterfeiter/v6" ) release-utils-0.8.5/log/000077500000000000000000000000001467053121000150625ustar00rootroot00000000000000release-utils-0.8.5/log/hooks.go000066400000000000000000000064071467053121000165430ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. 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 log import ( "fmt" "runtime" "strings" "github.com/sirupsen/logrus" ) type FileNameHook struct { field string skipPrefix []string formatter logrus.Formatter Formatter func(file, function string, line int) string } type wrapper struct { old logrus.Formatter hook *FileNameHook } // NewFilenameHook creates a new default FileNameHook. func NewFilenameHook() *FileNameHook { return &FileNameHook{ field: "file", skipPrefix: []string{"log/", "logrus/", "logrus@"}, Formatter: func(file, _ string, line int) string { return fmt.Sprintf("%s:%d", file, line) }, } } // Levels returns the levels for which the hook is activated. This contains // currently only the DebugLevel. func (f *FileNameHook) Levels() []logrus.Level { return []logrus.Level{logrus.DebugLevel} } // Fire executes the hook for every logrus entry. func (f *FileNameHook) Fire(entry *logrus.Entry) error { if f.formatter != entry.Logger.Formatter { f.formatter = &wrapper{entry.Logger.Formatter, f} } entry.Logger.Formatter = f.formatter return nil } // Format returns the log format including the caller as field. func (w *wrapper) Format(entry *logrus.Entry) ([]byte, error) { field := entry.WithField( w.hook.field, w.hook.Formatter(w.hook.findCaller()), ) field.Level = entry.Level field.Message = entry.Message return w.old.Format(field) } // findCaller returns the file, function and line number for the current call. func (f *FileNameHook) findCaller() (file, function string, line int) { var pc uintptr // The maximum amount of frames to be iterated const maxFrames = 10 for i := range maxFrames { // The amount of frames to be skipped to land at the actual caller const skipFrames = 5 pc, file, line = caller(skipFrames + i) if !f.shouldSkipPrefix(file) { break } } if pc != 0 { frames := runtime.CallersFrames([]uintptr{pc}) frame, _ := frames.Next() function = frame.Function } return file, function, line } // caller reports file and line number information about function invocations // on the calling goroutine's stack. The argument skip is the number of stack // frames to ascend, with 0 identifying the caller of Caller. func caller(skip int) (pc uintptr, file string, line int) { ok := false pc, file, line, ok = runtime.Caller(skip) if !ok { return 0, "", 0 } n := 0 for i := len(file) - 1; i > 0; i-- { if file[i] == '/' { n++ if n >= 2 { file = file[i+1:] break } } } return pc, file, line } // shouldSkipPrefix returns true if the hook should be skipped, otherwise false. func (f *FileNameHook) shouldSkipPrefix(file string) bool { for i := range f.skipPrefix { if strings.HasPrefix(file, f.skipPrefix[i]) { return true } } return false } release-utils-0.8.5/log/log.go000066400000000000000000000035251467053121000161770ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. 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 log import ( "fmt" "io" "os" "strings" "github.com/sirupsen/logrus" "sigs.k8s.io/release-utils/command" ) // SetupGlobalLogger uses to provided log level string and applies it globally. func SetupGlobalLogger(level string) error { logrus.SetFormatter(&logrus.TextFormatter{ DisableTimestamp: true, ForceColors: false, }) lvl, err := logrus.ParseLevel(level) if err != nil { return fmt.Errorf("setting log level to %s: %w", level, err) } logrus.SetLevel(lvl) if lvl >= logrus.DebugLevel { logrus.Debug("Setting commands globally into verbose mode") command.SetGlobalVerbose(true) } logrus.AddHook(NewFilenameHook()) logrus.Debugf("Using log level %q", lvl) return nil } // ToFile adds a file destination to the global logger. func ToFile(fileName string) error { file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0o755) if err != nil { return fmt.Errorf("open log file: %w", err) } writer := io.MultiWriter(logrus.StandardLogger().Out, file) logrus.SetOutput(writer) return nil } // LevelNames returns a comma separated list of available levels. func LevelNames() string { levels := []string{} for _, level := range logrus.AllLevels { levels = append(levels, fmt.Sprintf("'%s'", level.String())) } return strings.Join(levels, ", ") } release-utils-0.8.5/log/log_test.go000066400000000000000000000021571467053121000172360ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. 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 log_test import ( "os" "testing" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "sigs.k8s.io/release-utils/log" ) func TestToFile(t *testing.T) { file, err := os.CreateTemp("", "log-test-") require.NoError(t, err) defer os.Remove(file.Name()) require.NoError(t, log.SetupGlobalLogger("info")) require.NoError(t, log.ToFile(file.Name())) logrus.Info("test") content, err := os.ReadFile(file.Name()) require.NoError(t, err) require.Contains(t, string(content), "info") require.Contains(t, string(content), "test") } release-utils-0.8.5/log/step.go000066400000000000000000000022541467053121000163670ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. 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 log import ( "fmt" "github.com/sirupsen/logrus" ) // StepLogger is a step counting logger implementation. type StepLogger struct { *logrus.Logger steps uint currentStep uint } // NewStepLogger creates a new logger. func NewStepLogger(steps uint) *StepLogger { return &StepLogger{ Logger: logrus.StandardLogger(), steps: steps, currentStep: 0, } } // WithStep increments the internal step counter and adds the output to the // field. func (l *StepLogger) WithStep() *logrus.Entry { l.currentStep++ return l.WithField( "step", fmt.Sprintf("%d/%d", l.currentStep, l.steps), ) } release-utils-0.8.5/mage.go000066400000000000000000000012641467053121000155440ustar00rootroot00000000000000// +build ignore /* Copyright 2021 The Kubernetes Authors. 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 main import ( "os" "github.com/magefile/mage/mage" ) func main() { os.Exit(mage.Main()) } release-utils-0.8.5/mage/000077500000000000000000000000001467053121000152125ustar00rootroot00000000000000release-utils-0.8.5/mage/boilerplate.go000066400000000000000000000070241467053121000200460ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. 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 mage import ( "fmt" "log" "net/url" "os" "path" "path/filepath" "strings" "github.com/blang/semver/v4" "github.com/uwu-tools/magex/shx" kpath "k8s.io/utils/path" "sigs.k8s.io/release-utils/command" ) const ( // repo-infra (used for boilerplate script). defaultRepoInfraVersion = "v0.2.5" repoInfraURLBase = "https://raw.githubusercontent.com/kubernetes/repo-infra" ) // EnsureBoilerplateScript downloads copyright header boilerplate script, if // not already present in the repository. func EnsureBoilerplateScript(version, boilerplateScript string, forceInstall bool) error { found, err := kpath.Exists(kpath.CheckSymlinkOnly, boilerplateScript) if err != nil { return fmt.Errorf( "checking if copyright header boilerplate script (%s) exists: %w", boilerplateScript, err, ) } if !found || forceInstall { if version == "" { log.Printf( "A verify_boilerplate.py version to install was not specified. Using default version: %s", defaultRepoInfraVersion, ) version = defaultRepoInfraVersion } if !strings.HasPrefix(version, "v") { return fmt.Errorf( "repo-infra version (%s) must begin with a 'v'", version, ) } if _, err := semver.ParseTolerant(version); err != nil { return fmt.Errorf( "%s was not SemVer-compliant. Cannot continue.: %w", version, err, ) } binDir := filepath.Dir(boilerplateScript) if err := os.MkdirAll(binDir, 0o755); err != nil { return fmt.Errorf("creating binary directory: %w", err) } file, err := os.Create(boilerplateScript) if err != nil { return fmt.Errorf("creating file: %w", err) } defer file.Close() installURL, err := url.Parse(repoInfraURLBase) if err != nil { return fmt.Errorf("parsing URL: %w", err) } installURL.Path = path.Join( installURL.Path, version, "hack", "verify_boilerplate.py", ) installCmd := command.New( "curl", "-sSfL", installURL.String(), "-o", boilerplateScript, ) err = installCmd.RunSuccess() if err != nil { return fmt.Errorf("installing verify_boilerplate.py: %w", err) } } if err := os.Chmod(boilerplateScript, 0o755); err != nil { return fmt.Errorf("making script executable: %w", err) } return nil } // VerifyBoilerplate runs copyright header checks. func VerifyBoilerplate(version, binDir, boilerplateDir string, forceInstall bool) error { if _, err := kpath.Exists(kpath.CheckSymlinkOnly, boilerplateDir); err != nil { return fmt.Errorf( "checking if copyright header boilerplate directory (%s) exists: %w", boilerplateDir, err, ) } boilerplateScript := filepath.Join(binDir, "verify_boilerplate.py") if err := EnsureBoilerplateScript(version, boilerplateScript, forceInstall); err != nil { return fmt.Errorf("ensuring copyright header script is installed: %w", err) } if err := shx.RunV( boilerplateScript, "--boilerplate-dir", boilerplateDir, ); err != nil { return fmt.Errorf("running copyright header checks: %w", err) } return nil } release-utils-0.8.5/mage/cosign.go000066400000000000000000000032531467053121000170260ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. 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 mage import ( "fmt" "log" "runtime" "github.com/uwu-tools/magex/pkg" "github.com/uwu-tools/magex/pkg/downloads" ) const defaultCosignVersion = "v2.2.4" // EnsureCosign makes sure that the specified cosign version is available. func EnsureCosign(version string) error { if version == "" { version = defaultCosignVersion } log.Printf("Checking if `cosign` version %s is installed\n", version) found, err := pkg.IsCommandAvailable("cosign", "version", version) if err != nil { return err } if !found { fmt.Println("`cosign` not found") return InstallCosign(version) } fmt.Println("`cosign` is installed!") return nil } // InstallCosign installs the required cosign version. func InstallCosign(version string) error { fmt.Println("Will install `cosign`") target := "cosign" if runtime.GOOS == "windows" { target = "cosign.exe" } opts := downloads.DownloadOptions{ UrlTemplate: "https://github.com/sigstore/cosign/releases/download/{{.VERSION}}/cosign-{{.GOOS}}-{{.GOARCH}}", Name: target, Version: version, Ext: "", } return downloads.DownloadToGopathBin(opts) } release-utils-0.8.5/mage/dependency.go000066400000000000000000000070271467053121000176650ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. 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 mage import ( "fmt" "log" "github.com/blang/semver/v4" "github.com/uwu-tools/magex/pkg" "github.com/uwu-tools/magex/shx" ) const ( // zeitgeist. defaultZeitgeistVersion = "v0.5.3" zeitgeistCmd = "zeitgeist" zeitgeistModule = "sigs.k8s.io/zeitgeist" zeitgeistRemoteModule = "sigs.k8s.io/zeitgeist/remote/zeitgeist" ) // Ensure zeitgeist is installed and on the PATH. func EnsureZeitgeist(version string) error { if version == "" { log.Printf( "A zeitgeist version to install was not specified. Using default version: %s", defaultZeitgeistVersion, ) version = defaultZeitgeistVersion } if _, err := semver.ParseTolerant(version); err != nil { return fmt.Errorf( "%s was not SemVer-compliant, cannot continue: %w", version, err, ) } if err := pkg.EnsurePackageWith(pkg.EnsurePackageOptions{ Name: zeitgeistModule, DefaultVersion: version, VersionCommand: "version", }); err != nil { return fmt.Errorf("ensuring package: %w", err) } return nil } // Ensure zeitgeist remote is installed and on the PATH. func EnsureZeitgeistRemote(version string) error { if version == "" { log.Printf( "A zeitgeist remote version to install was not specified. Using default version: %s", defaultZeitgeistVersion, ) version = defaultZeitgeistVersion } if _, err := semver.ParseTolerant(version); err != nil { return fmt.Errorf( "%s was not SemVer-compliant, cannot continue: %w", version, err, ) } if err := pkg.EnsurePackageWith(pkg.EnsurePackageOptions{ Name: zeitgeistRemoteModule, DefaultVersion: version, VersionCommand: "version", }); err != nil { return fmt.Errorf("ensuring package: %w", err) } return nil } // VerifyDeps runs zeitgeist to verify dependency versions. func VerifyDeps(version, basePath, configPath string, localOnly bool) error { if err := EnsureZeitgeist(version); err != nil { return fmt.Errorf("ensuring zeitgeist is installed: %w", err) } args := []string{"validate"} if localOnly { args = append(args, "--local-only") } if basePath != "" { args = append(args, "--base-path", basePath) } if configPath != "" { args = append(args, "--config", configPath) } if err := shx.RunV(zeitgeistCmd, args...); err != nil { return fmt.Errorf("running zeitgeist: %w", err) } return nil } /* ##@ Dependencies .SILENT: update-deps update-deps-go update-mocks .PHONY: update-deps update-deps-go update-mocks update-deps: update-deps-go ## Update all dependencies for this repo echo -e "${COLOR}Commit/PR the following changes:${NOCOLOR}" git status --short update-deps-go: GO111MODULE=on update-deps-go: ## Update all golang dependencies for this repo go get -u -t ./... go mod tidy go mod verify $(MAKE) test-go-unit ./scripts/update-all.sh update-mocks: ## Update all generated mocks go generate ./... for f in $(shell find . -name fake_*.go); do \ cp scripts/boilerplate/boilerplate.generatego.txt tmp ;\ cat $$f >> tmp ;\ mv tmp $$f ;\ done */ release-utils-0.8.5/mage/git.go000066400000000000000000000040461467053121000163300ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. 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 mage import ( "fmt" "sigs.k8s.io/release-utils/command" ) const ( gitConfigNameKey = "user.name" gitConfigNameValue = "releng-ci-user" gitConfigEmailKey = "user.email" gitConfigEmailValue = "nobody@k8s.io" ) func CheckGitConfigExists() bool { userName := command.New( "git", "config", "--global", "--get", gitConfigNameKey, ) stream, err := userName.RunSilentSuccessOutput() if err != nil || stream.OutputTrimNL() == "" { // NB: We're intentionally ignoring the error here because 'git config' // returns an error (result code -1) if the config doesn't exist. return false } userEmail := command.New( "git", "config", "--global", "--get", gitConfigEmailKey, ) stream, err = userEmail.RunSilentSuccessOutput() if err != nil || stream.OutputTrimNL() == "" { // NB: We're intentionally ignoring the error here because 'git config' // returns an error (result code -1) if the config doesn't exist. return false } return true } func EnsureGitConfig() error { exists := CheckGitConfigExists() if exists { return nil } if err := command.New( "git", "config", "--global", gitConfigNameKey, gitConfigNameValue, ).RunSuccess(); err != nil { return fmt.Errorf("configuring git %s: %w", gitConfigNameKey, err) } if err := command.New( "git", "config", "--global", gitConfigEmailKey, gitConfigEmailValue, ).RunSuccess(); err != nil { return fmt.Errorf("configuring git %s: %w", gitConfigEmailKey, err) } return nil } release-utils-0.8.5/mage/golangci-lint.go000066400000000000000000000122721467053121000202740ustar00rootroot00000000000000/* Copyright 2021 The Kubernetes Authors. 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 mage import ( "fmt" "log" "net/url" "os" "path" "path/filepath" "strings" "github.com/blang/semver/v4" "github.com/uwu-tools/magex/pkg" "github.com/uwu-tools/magex/pkg/gopath" "github.com/uwu-tools/magex/shx" kpath "k8s.io/utils/path" "sigs.k8s.io/release-utils/command" "sigs.k8s.io/release-utils/env" ) const ( // golangci-lint. defaultGolangCILintVersion = "v1.61.0" golangciCmd = "golangci-lint" golangciConfig = ".golangci.yml" golangciURLBase = "https://raw.githubusercontent.com/golangci/golangci-lint" defaultMinGoVersion = "1.21" ) // Ensure golangci-lint is installed and on the PATH. func EnsureGolangCILint(version string, forceInstall bool) error { found, err := pkg.IsCommandAvailable(golangciCmd, "--version", version) if err != nil { return fmt.Errorf( "checking if %s is available: %w", golangciCmd, err, ) } if !found || forceInstall { if version == "" { log.Printf( "A golangci-lint version to install was not specified. Using default version: %s", defaultGolangCILintVersion, ) version = defaultGolangCILintVersion } if !strings.HasPrefix(version, "v") { return fmt.Errorf( "golangci-lint version (%s) must begin with a 'v'", version, ) } if _, err := semver.ParseTolerant(version); err != nil { return fmt.Errorf( "%s was not SemVer-compliant. Cannot continue.: %w", version, err, ) } installURL, err := url.Parse(golangciURLBase) if err != nil { return fmt.Errorf("parsing URL: %w", err) } installURL.Path = path.Join(installURL.Path, version, "install.sh") err = gopath.EnsureGopathBin() if err != nil { return fmt.Errorf("ensuring $GOPATH/bin: %w", err) } gopathBin := gopath.GetGopathBin() installCmd := command.New( "curl", "-sSfL", installURL.String(), ).Pipe( "sh", "-s", "--", "-b", gopathBin, version, ) err = installCmd.RunSuccess() if err != nil { return fmt.Errorf("installing golangci-lint: %w", err) } } return nil } // RunGolangCILint runs all golang linters. func RunGolangCILint(version string, forceInstall bool, args ...string) error { if _, err := kpath.Exists(kpath.CheckSymlinkOnly, golangciConfig); err != nil { return fmt.Errorf( "checking if golangci-lint config file (%s) exists: %w", golangciConfig, err, ) } if err := EnsureGolangCILint(version, forceInstall); err != nil { return fmt.Errorf("ensuring golangci-lint is installed: %w", err) } if err := shx.RunV(golangciCmd, "version"); err != nil { return fmt.Errorf("getting golangci-lint version: %w", err) } if err := shx.RunV(golangciCmd, "linters"); err != nil { return fmt.Errorf("listing golangci-lint linters: %w", err) } runArgs := []string{"run"} runArgs = append(runArgs, args...) if err := shx.RunV(golangciCmd, runArgs...); err != nil { return fmt.Errorf("running golangci-lint linters: %w", err) } return nil } func TestGo(verbose bool, pkgs ...string) error { return testGo(verbose, "", pkgs...) } func TestGoWithTags(verbose bool, tags string, pkgs ...string) error { return testGo(verbose, tags, pkgs...) } func testGo(verbose bool, tags string, pkgs ...string) error { verboseFlag := "" if verbose { verboseFlag = "-v" } pkgArgs := []string{} if len(pkgs) > 0 { for _, p := range pkgs { pkgArg := fmt.Sprintf("./%s/...", p) pkgArgs = append(pkgArgs, pkgArg) } } else { pkgArgs = []string{"./..."} } cmdArgs := []string{"test"} cmdArgs = append(cmdArgs, verboseFlag) if tags != "" { cmdArgs = append(cmdArgs, "-tags", tags) } cmdArgs = append(cmdArgs, pkgArgs...) if err := shx.RunV( "go", cmdArgs..., ); err != nil { return fmt.Errorf("running go test: %w", err) } return nil } // VerifyGoMod runs `go mod tidy` and `git diff --exit-code go.*` to ensure // all module updates have been checked in. func VerifyGoMod() error { minGoVersion := env.Default("MIN_GO_VERSION", defaultMinGoVersion) if err := shx.RunV( "go", "mod", "tidy", "-compat="+minGoVersion, ); err != nil { return fmt.Errorf("running go mod tidy: %w", err) } if err := shx.RunV("git", "diff", "--exit-code", "go.*"); err != nil { return fmt.Errorf("running go mod tidy: %w", err) } return nil } // VerifyBuild builds the project for a chosen set of platforms. func VerifyBuild(scriptDir string) error { wd, err := os.Getwd() if err != nil { return fmt.Errorf("getting working directory: %w", err) } scriptDir = filepath.Join(wd, scriptDir) buildScript := filepath.Join(scriptDir, "verify-build.sh") if err := shx.RunV(buildScript); err != nil { return fmt.Errorf("running go build: %w", err) } return nil } release-utils-0.8.5/mage/ko.go000066400000000000000000000037721467053121000161630ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. 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 mage import ( "fmt" "runtime" "github.com/uwu-tools/magex/pkg" "github.com/uwu-tools/magex/pkg/archive" "github.com/uwu-tools/magex/pkg/downloads" ) const defaultKoVersion = "0.15.2" // EnsureKO ensures that the ko binary exists. func EnsureKO(version string) error { if version == "" { version = defaultKoVersion } fmt.Printf("Checking if `ko` version %s is installed\n", version) found, err := pkg.IsCommandAvailable("ko", "version", version) if err != nil { return err } if !found { fmt.Println("`ko` not found") return InstallKO(version) } fmt.Println("`ko` is installed!") return nil } // Maybe we can move this to release-utils. func InstallKO(version string) error { fmt.Println("Will install `ko`") target := "ko" if runtime.GOOS == "windows" { target = "ko.exe" } opts := archive.DownloadArchiveOptions{ DownloadOptions: downloads.DownloadOptions{ UrlTemplate: "https://github.com/ko-build/ko/releases/download/v{{.VERSION}}/ko_{{.VERSION}}_{{.GOOS}}_{{.GOARCH}}{{.EXT}}", Name: "ko", Version: version, OsReplacement: map[string]string{ "darwin": "Darwin", "linux": "Linux", "windows": "Windows", }, ArchReplacement: map[string]string{ "amd64": "x86_64", }, }, ArchiveExtensions: map[string]string{ "linux": ".tar.gz", "darwin": ".tar.gz", "windows": ".tar.gz", }, TargetFileTemplate: target, } return archive.DownloadToGopathBin(opts) } release-utils-0.8.5/mage/version.go000066400000000000000000000044711467053121000172340ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. 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 mage import ( "fmt" "strconv" "time" "github.com/uwu-tools/magex/shx" ) // getVersion gets a description of the commit, e.g. v0.30.1 (latest) or v0.30.1-32-gfe72ff73 (canary). func getVersion() (string, error) { version, err := shx.Output("git", "describe", "--tags", "--always") if err != nil { return "", err } if version != "" { return version, nil } // repo without any tags in it return "v0.0.0", nil } // getCommit gets the hash of the current commit. func getCommit() (string, error) { return shx.Output("git", "rev-parse", "--short", "HEAD") } // getGitState gets the state of the git repository. func getGitState() string { _, err := shx.Output("git", "diff", "--quiet") if err != nil { return "dirty" } return "clean" } // getBuildDateTime gets the build date and time. func getBuildDateTime() (string, error) { result, err := shx.Output("git", "log", "-1", "--pretty=%ct") if err != nil { return "", err } if result != "" { parsedInt, err := strconv.ParseInt(result, 10, 64) if err != nil { return "", fmt.Errorf("parse source date epoch to int: %w", err) } return time.Unix(parsedInt, 0).UTC().Format(time.RFC3339), nil } return shx.Output("date", "+%Y-%m-%dT%H:%M:%SZ") } // GenerateLDFlags returns the string to use in the `-ldflags` flag. func GenerateLDFlags() (string, error) { pkg := "sigs.k8s.io/release-utils/version" version, err := getVersion() if err != nil { return "", err } commit, err := getCommit() if err != nil { return "", err } buildTime, err := getBuildDateTime() if err != nil { return "", err } return fmt.Sprintf("-X %[1]s.gitVersion=%[2]s -X %[1]s.gitCommit=%[3]s -X %[1]s.gitTreeState=%[4]s -X %[1]s.buildDate=%[5]s", pkg, version, commit, getGitState(), buildTime), nil } release-utils-0.8.5/mage/version_test.go000066400000000000000000000015051467053121000202660ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. 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 mage import "testing" func TestGenerateLDFlags(t *testing.T) { got, err := GenerateLDFlags() if err != nil { t.Errorf("failed to generate ld flags: %v", err) } if got == "" { t.Errorf("GenerateLDFlags() failed to return a string") } t.Log(got) } release-utils-0.8.5/magefile.go000066400000000000000000000036071467053121000164070ustar00rootroot00000000000000//go:build mage // +build mage /* Copyright 2021 The Kubernetes Authors. 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 main import ( "fmt" "path/filepath" "sigs.k8s.io/release-utils/mage" ) // Default target to run when none is specified // If not set, running mage will list available targets var Default = Verify const ( binDir = "bin" scriptDir = "scripts" ) var boilerplateDir = filepath.Join(scriptDir, "boilerplate") // All runs all targets for this repository func All() error { if err := Verify(); err != nil { return err } if err := Test(); err != nil { return err } return nil } // Test runs various test functions func Test() error { if err := mage.TestGo(true); err != nil { return err } return nil } // Verify runs repository verification scripts func Verify() error { fmt.Println("Running copyright header checks...") if err := mage.VerifyBoilerplate("", binDir, boilerplateDir, true); err != nil { return err } fmt.Println("Running external dependency checks...") if err := mage.VerifyDeps("", "", "", true); err != nil { return err } fmt.Println("Running go module linter...") if err := mage.VerifyGoMod(); err != nil { return err } fmt.Println("Running golangci-lint...") if err := mage.RunGolangCILint("", false); err != nil { return err } fmt.Println("Running go build...") if err := mage.VerifyBuild(scriptDir); err != nil { return err } return nil } release-utils-0.8.5/scripts/000077500000000000000000000000001467053121000157705ustar00rootroot00000000000000release-utils-0.8.5/scripts/boilerplate/000077500000000000000000000000001467053121000202725ustar00rootroot00000000000000release-utils-0.8.5/scripts/boilerplate/boilerplate.Dockerfile.txt000066400000000000000000000011141467053121000254000ustar00rootroot00000000000000# Copyright YEAR The Kubernetes Authors. # # 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. release-utils-0.8.5/scripts/boilerplate/boilerplate.Makefile.txt000066400000000000000000000011141467053121000250460ustar00rootroot00000000000000# Copyright YEAR The Kubernetes Authors. # # 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. release-utils-0.8.5/scripts/boilerplate/boilerplate.generatego.txt000066400000000000000000000010661467053121000254570ustar00rootroot00000000000000/* Copyright The Kubernetes Authors. 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. */ release-utils-0.8.5/scripts/boilerplate/boilerplate.go.txt000066400000000000000000000010731467053121000237420ustar00rootroot00000000000000/* Copyright YEAR The Kubernetes Authors. 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. */ release-utils-0.8.5/scripts/boilerplate/boilerplate.goheader.txt000066400000000000000000000010421467053121000251070ustar00rootroot00000000000000{{copyright-holder}} 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. release-utils-0.8.5/scripts/boilerplate/boilerplate.py.txt000066400000000000000000000011141467053121000237610ustar00rootroot00000000000000# Copyright YEAR The Kubernetes Authors. # # 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. release-utils-0.8.5/scripts/boilerplate/boilerplate.sh.txt000066400000000000000000000011141467053121000237430ustar00rootroot00000000000000# Copyright YEAR The Kubernetes Authors. # # 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. release-utils-0.8.5/scripts/verify-build.sh000077500000000000000000000017761467053121000207430ustar00rootroot00000000000000#!/usr/bin/env bash # Copyright 2021 The Kubernetes Authors. # # 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. set -o errexit set -o nounset set -o pipefail PLATFORMS=( linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le linux/s390x windows/amd64 windows/386 freebsd/amd64 darwin/amd64 ) for PLATFORM in "${PLATFORMS[@]}"; do OS="${PLATFORM%/*}" ARCH=$(basename "$PLATFORM") echo "Building project for $PLATFORM" GOARCH="$ARCH" GOOS="$OS" go build ./... done release-utils-0.8.5/tar/000077500000000000000000000000001467053121000150675ustar00rootroot00000000000000release-utils-0.8.5/tar/tar.go000066400000000000000000000176431467053121000162170ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. 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 tar import ( "archive/tar" "compress/gzip" "fmt" "io" "os" "path/filepath" "regexp" "strings" "github.com/sirupsen/logrus" ) // Compress the provided `tarContentsPath` into the `tarFilePath` while // excluding the `exclude` regular expression patterns. This function will // preserve path between `tarFilePath` and `tarContentsPath` directories inside // the archive (see `CompressWithoutPreservingPath` as an alternative). func Compress(tarFilePath, tarContentsPath string, excludes ...*regexp.Regexp) error { return compress(true, tarFilePath, tarContentsPath, excludes...) } // Compress the provided `tarContentsPath` into the `tarFilePath` while // excluding the `exclude` regular expression patterns. This function will // not preserve path leading to the `tarContentsPath` directory in the archive. func CompressWithoutPreservingPath(tarFilePath, tarContentsPath string, excludes ...*regexp.Regexp) error { return compress(false, tarFilePath, tarContentsPath, excludes...) } func compress(preserveRootDirStructure bool, tarFilePath, tarContentsPath string, excludes ...*regexp.Regexp) error { tarFile, err := os.Create(tarFilePath) if err != nil { return fmt.Errorf("create tar file %q: %w", tarFilePath, err) } defer tarFile.Close() gzipWriter := gzip.NewWriter(tarFile) defer gzipWriter.Close() tarWriter := tar.NewWriter(gzipWriter) defer tarWriter.Close() if err := filepath.Walk(tarContentsPath, func(filePath string, fileInfo os.FileInfo, err error) error { if err != nil { return err } var link string isLink := fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink if isLink { link, err = os.Readlink(filePath) if err != nil { return fmt.Errorf("read file link of %s: %w", filePath, err) } } header, err := tar.FileInfoHeader(fileInfo, link) if err != nil { return fmt.Errorf("create file info header for %q: %w", filePath, err) } if fileInfo.IsDir() || filePath == tarFilePath { logrus.Tracef("Skipping: %s", filePath) return nil } for _, re := range excludes { if re != nil && re.MatchString(filePath) { logrus.Tracef("Excluding: %s", filePath) return nil } } // Make the path inside the tar relative to the archive path if // necessary. // // The default way this works is that we preserve the path between // `tarFilePath` and `tarContentsPath` directories inside the archive. // This might not work well if `tarFilePath` and `tarContentsPath` // are on different levels in the file system (e.g. they don't have // common parent directory). // In such case we can disable `preserveRootDirStructure` flag which // will make paths inside the archive relative to `tarContentsPath`. dropPath := filepath.Dir(tarFilePath) if !preserveRootDirStructure { dropPath = tarContentsPath } header.Name = strings.TrimLeft( strings.TrimPrefix(filePath, dropPath), string(filepath.Separator), ) header.Linkname = filepath.ToSlash(header.Linkname) if err := tarWriter.WriteHeader(header); err != nil { return fmt.Errorf("writing tar header: %w", err) } if !isLink { file, err := os.Open(filePath) if err != nil { return fmt.Errorf("open file %q: %w", filePath, err) } if _, err := io.Copy(tarWriter, file); err != nil { return fmt.Errorf("writing file to tar writer: %w", err) } file.Close() } return nil }); err != nil { return fmt.Errorf("walking tree in %q: %w", tarContentsPath, err) } return nil } // Extract can be used to extract the provided `tarFilePath` into the // `destinationPath`. func Extract(tarFilePath, destinationPath string) error { return iterateTarball( tarFilePath, func(reader *tar.Reader, header *tar.Header) (stop bool, err error) { switch header.Typeflag { case tar.TypeDir: targetDir, err := SanitizeArchivePath(destinationPath, header.Name) if err != nil { return false, fmt.Errorf("SanitizeArchivePath: %w", err) } logrus.Tracef("Creating directory %s", targetDir) if err := os.MkdirAll(targetDir, os.FileMode(0o755)); err != nil { return false, fmt.Errorf("create target directory: %w", err) } case tar.TypeSymlink: targetFile, err := SanitizeArchivePath(destinationPath, header.Name) if err != nil { return false, fmt.Errorf("SanitizeArchivePath: %w", err) } logrus.Tracef( "Creating symlink %s -> %s", header.Linkname, targetFile, ) if err := os.MkdirAll( filepath.Dir(targetFile), os.FileMode(0o755), ); err != nil { return false, fmt.Errorf("create target directory: %w", err) } if err := os.Symlink(header.Linkname, targetFile); err != nil { return false, fmt.Errorf("create symlink: %w", err) } // tar.TypeRegA has been deprecated since Go 1.11 // should we just remove? case tar.TypeReg: targetFile, err := SanitizeArchivePath(destinationPath, header.Name) if err != nil { return false, fmt.Errorf("SanitizeArchivePath: %w", err) } logrus.Tracef("Creating file %s", targetFile) if err := os.MkdirAll( filepath.Dir(targetFile), os.FileMode(0o755), ); err != nil { return false, fmt.Errorf("create target directory: %w", err) } outFile, err := os.Create(targetFile) if err != nil { return false, fmt.Errorf("create target file: %w", err) } //nolint:gosec // integer overflow highly unlikely if err := outFile.Chmod(os.FileMode(header.Mode)); err != nil { return false, fmt.Errorf("chmod target file: %w", err) } if _, err := io.Copy(outFile, reader); err != nil { return false, fmt.Errorf("copy file contents %s: %w", targetFile, err) } outFile.Close() default: logrus.Warnf( "File %s has unknown type %s", header.Name, string(header.Typeflag), ) } return false, nil }, ) } // Sanitize archive file pathing from "G305: Zip Slip vulnerability" // https://security.snyk.io/research/zip-slip-vulnerability func SanitizeArchivePath(d, t string) (v string, err error) { v = filepath.Join(d, t) if strings.HasPrefix(v, filepath.Clean(d)) { return v, nil } return "", fmt.Errorf("%s: %s", "content filepath is tainted", t) } // ReadFileFromGzippedTar opens a tarball and reads contents of a file inside. func ReadFileFromGzippedTar( tarPath, filePath string, ) (res io.Reader, err error) { if err := iterateTarball( tarPath, func(reader *tar.Reader, header *tar.Header) (stop bool, err error) { if header.Name == filePath { res = reader return true, nil } return false, nil }, ); err != nil { return nil, err } if res == nil { return nil, fmt.Errorf("unable to find file %q in tarball %q: %w", tarPath, filePath, err) } return res, nil } // iterateTarball can be used to iterate over the contents of a tarball by // calling the callback for each entry. func iterateTarball( tarPath string, callback func(*tar.Reader, *tar.Header) (stop bool, err error), ) error { file, err := os.Open(tarPath) if err != nil { return fmt.Errorf("opening tar file %q: %w", tarPath, err) } gzipReader, err := gzip.NewReader(file) if err != nil { return fmt.Errorf("creating gzip reader for file %q: %w", tarPath, err) } tarReader := tar.NewReader(gzipReader) for { tarHeader, err := tarReader.Next() if err == io.EOF { break // End of archive } stop, err := callback(tarReader, tarHeader) if err != nil { return err } if stop { // User wants to stop break } } return nil } release-utils-0.8.5/tar/tar_test.go000066400000000000000000000144541467053121000172530ustar00rootroot00000000000000/* Copyright 2020 The Kubernetes Authors. 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 tar import ( "archive/tar" "io" "os" "path/filepath" "regexp" "testing" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" ) func TestCompress(t *testing.T) { baseTmpDir, err := os.MkdirTemp("", "compress-") require.NoError(t, err) defer os.RemoveAll(baseTmpDir) for _, fileName := range []string{ "1.txt", "2.bin", "3.md", } { require.NoError(t, os.WriteFile( filepath.Join(baseTmpDir, fileName), []byte{1, 2, 3}, os.FileMode(0o644), )) } subTmpDir := filepath.Join(baseTmpDir, "sub") require.NoError(t, os.MkdirAll(subTmpDir, os.FileMode(0o755))) for _, fileName := range []string{ "4.txt", "5.bin", "6.md", } { require.NoError(t, os.WriteFile( filepath.Join(subTmpDir, fileName), []byte{4, 5, 6}, os.FileMode(0o644), )) } logrus.SetLevel(logrus.DebugLevel) require.NoError(t, os.Symlink( filepath.Join(baseTmpDir, "1.txt"), filepath.Join(subTmpDir, "link"), )) excludes := []*regexp.Regexp{ regexp.MustCompile(".md"), regexp.MustCompile("5"), } tarFilePath := filepath.Join(baseTmpDir, "res.tar.gz") require.NoError(t, Compress(tarFilePath, baseTmpDir, excludes...)) require.FileExists(t, tarFilePath) res := []string{"1.txt", "2.bin", "sub/4.txt", "sub/link"} require.NoError(t, iterateTarball( tarFilePath, func(_ *tar.Reader, header *tar.Header) (bool, error) { require.Equal(t, res[0], header.Name) res = res[1:] return false, nil }), ) } func TestCompressWithoutPreservingPath(t *testing.T) { baseTmpDir, err := os.MkdirTemp("", "compress-") require.NoError(t, err) defer os.RemoveAll(baseTmpDir) compressDir := filepath.Join(baseTmpDir, "to_compress") require.NoError(t, os.MkdirAll(compressDir, os.FileMode(0o755))) for _, fileName := range []string{ "1.txt", "2.bin", "3.md", } { require.NoError(t, os.WriteFile( filepath.Join(compressDir, fileName), []byte{1, 2, 3}, os.FileMode(0o644), )) } logrus.SetLevel(logrus.DebugLevel) tarFilePath := filepath.Join(baseTmpDir, "res.tar.gz") require.NoError(t, CompressWithoutPreservingPath(tarFilePath, compressDir)) require.FileExists(t, tarFilePath) res := []string{"1.txt", "2.bin", "3.md"} require.NoError(t, iterateTarball( tarFilePath, func(_ *tar.Reader, header *tar.Header) (bool, error) { require.Equal(t, res[0], header.Name) res = res[1:] return false, nil }), ) } func TestExtract(t *testing.T) { tarball := []byte{ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xec, 0xd7, 0xdf, 0xea, 0x82, 0x30, 0x14, 0xc0, 0xf1, 0xfd, 0xfe, 0xd4, 0x73, 0xf4, 0x02, 0xb9, 0xb3, 0x9d, 0xb9, 0xd9, 0xe3, 0xa8, 0x08, 0x49, 0x69, 0xe2, 0x26, 0xf4, 0xf8, 0x31, 0x93, 0x2e, 0xba, 0x10, 0x8a, 0xe6, 0x08, 0xcf, 0xe7, 0x66, 0x0c, 0xc4, 0x1d, 0x2f, 0xbe, 0x30, 0x45, 0xe2, 0xae, 0x8e, 0x85, 0x05, 0x00, 0xa0, 0x95, 0xf2, 0xab, 0x30, 0x29, 0x8c, 0x7b, 0x71, 0xdf, 0x4f, 0x90, 0x09, 0x34, 0x29, 0x00, 0x48, 0xd4, 0x9a, 0x81, 0x90, 0x0a, 0x91, 0xed, 0x20, 0xf0, 0x5c, 0xa3, 0xc1, 0xba, 0xbc, 0x67, 0x00, 0x36, 0xb7, 0xe5, 0x31, 0x9f, 0x7b, 0xae, 0xea, 0xed, 0xcc, 0x7b, 0xa6, 0x2f, 0x79, 0xac, 0x5f, 0xe2, 0xe7, 0xf7, 0x2f, 0xf6, 0x08, 0x24, 0x22, 0x99, 0x14, 0x75, 0x1b, 0xf8, 0x8c, 0x37, 0xfa, 0x47, 0x9d, 0x52, 0xff, 0x4b, 0xa0, 0xfe, 0xd7, 0xcd, 0x0e, 0x05, 0x57, 0x81, 0xef, 0x00, 0xaf, 0xf7, 0x8f, 0x52, 0x1a, 0xea, 0x7f, 0x09, 0xff, 0x9b, 0x6d, 0xec, 0x11, 0x48, 0x44, 0xbe, 0xff, 0x73, 0xdd, 0x9e, 0x42, 0x9e, 0xe1, 0x7b, 0x30, 0xc6, 0xcc, 0xf4, 0x0f, 0x4f, 0xfd, 0x1b, 0xed, 0xef, 0xff, 0x92, 0xbb, 0xa6, 0xe3, 0xe5, 0xa5, 0xe9, 0xfa, 0xca, 0xda, 0xfd, 0x01, 0x33, 0x25, 0x54, 0x86, 0x8a, 0x7f, 0xf2, 0xa7, 0x65, 0xe5, 0xfd, 0x13, 0x42, 0xd6, 0xeb, 0x16, 0x00, 0x00, 0xff, 0xff, 0xe9, 0xde, 0xbe, 0xdf, 0x00, 0x12, 0x00, 0x00, } file, err := os.CreateTemp("", "tarball") require.NoError(t, err) defer os.Remove(file.Name()) _, err = file.Write(tarball) require.NoError(t, err) baseTmpDir, err := os.MkdirTemp("", "extract-") require.NoError(t, err) require.NoError(t, os.RemoveAll(baseTmpDir)) defer os.RemoveAll(baseTmpDir) require.NoError(t, Extract(file.Name(), baseTmpDir)) res := []string{ filepath.Base(baseTmpDir), "1.txt", "2.bin", "sub", "4.txt", "link", } require.NoError(t, filepath.Walk( baseTmpDir, func(_ string, fileInfo os.FileInfo, _ error) error { require.Equal(t, res[0], fileInfo.Name()) if res[0] == "link" { require.Equal(t, os.ModeSymlink, fileInfo.Mode()&os.ModeSymlink) } res = res[1:] return nil }, )) } func TestReadFileFromGzippedTar(t *testing.T) { baseTmpDir, err := os.MkdirTemp("", "tar-read-file-") require.NoError(t, err) defer os.RemoveAll(baseTmpDir) const ( testFilePath = "test.txt" testFileContents = "test-file-contents" ) testTarPath := filepath.Join(baseTmpDir, "test.tar.gz") require.NoError(t, os.WriteFile( filepath.Join(baseTmpDir, testFilePath), []byte(testFileContents), os.FileMode(0o644), )) require.NoError(t, Compress(testTarPath, baseTmpDir, nil)) type args struct { tarPath string filePath string } type want struct { fileContents string shouldErr bool } cases := map[string]struct { args args want want }{ "FoundFileInTar": { args: args{ tarPath: testTarPath, filePath: testFilePath, }, want: want{fileContents: testFileContents}, }, "FileNotInTar": { args: args{ tarPath: testTarPath, filePath: "badfile.txt", }, want: want{shouldErr: true}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { r, err := ReadFileFromGzippedTar(tc.args.tarPath, tc.args.filePath) if tc.want.shouldErr { require.Nil(t, r) require.Error(t, err) } else { file, err := io.ReadAll(r) require.NoError(t, err) require.Equal(t, tc.want.fileContents, string(file)) } }) } } release-utils-0.8.5/util/000077500000000000000000000000001467053121000152565ustar00rootroot00000000000000release-utils-0.8.5/util/common.go000066400000000000000000000370301467053121000171000ustar00rootroot00000000000000/* Copyright 2019 The Kubernetes Authors. 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 util import ( "bufio" "errors" "fmt" "io" "os" "os/signal" "path/filepath" "regexp" "strings" "syscall" "github.com/blang/semver/v4" "github.com/sirupsen/logrus" "sigs.k8s.io/release-utils/command" ) const ( TagPrefix = "v" ) var ( regexpCRLF = regexp.MustCompile(`\015$`) regexpCtrlChar = regexp.MustCompile(`\x1B[\[(](\d{1,2}(;\d{1,2})?)?[mKB]`) regexpOauthToken = regexp.MustCompile(`[a-f0-9]{40}:x-oauth-basic`) regexpGitToken = regexp.MustCompile(`git:[a-f0-9]{35,40}@github\.com`) ) // UserInputError a custom error to handle more user input info. type UserInputError struct { ErrorString string isCtrlC bool } // Error return the error string. func (e UserInputError) Error() string { return e.ErrorString } // IsCtrlC return true if the user has hit Ctrl+C. func (e UserInputError) IsCtrlC() bool { return e.isCtrlC } // NewUserInputError creates a new UserInputError. func NewUserInputError(message string, ctrlC bool) UserInputError { return UserInputError{ ErrorString: message, isCtrlC: ctrlC, } } // PackagesAvailable takes a slice of packages and determines if they are installed // on the host OS. Replaces common::check_packages. func PackagesAvailable(packages ...string) (bool, error) { type packageVerifier struct { cmd string args []string } type packageChecker struct { manager string verifier *packageVerifier } var checker *packageChecker for _, x := range []struct { possiblePackageManagers []string verifierCmd string verifierArgs []string }{ { // Debian, Ubuntu and similar []string{"apt"}, "dpkg", []string{"-l"}, }, { // Fedora, openSUSE and similar []string{"dnf", "yum", "zypper"}, "rpm", []string{"--quiet", "-q"}, }, { // ArchLinux and similar []string{"yay", "pacaur", "pacman"}, "pacman", []string{"-Qs"}, }, } { // Find a working package verifier if !command.Available(x.verifierCmd) { logrus.Debugf("Skipping not available package verifier %s", x.verifierCmd) continue } // Find a working package manager packageManager := "" for _, mgr := range x.possiblePackageManagers { if command.Available(mgr) { packageManager = mgr break } logrus.Debugf("Skipping not available package manager %s", mgr) } if packageManager == "" { return false, fmt.Errorf( "unable to find working package manager for verifier `%s`", x.verifierCmd, ) } checker = &packageChecker{ manager: packageManager, verifier: &packageVerifier{x.verifierCmd, x.verifierArgs}, } break } if checker == nil { return false, errors.New("unable to find working package manager") } logrus.Infof("Assuming %q as package manager", checker.manager) missingPkgs := []string{} for _, pkg := range packages { logrus.Infof("Checking if %q has been installed", pkg) args := checker.verifier.args args = append(args, pkg) if err := command.New(checker.verifier.cmd, args...). RunSilentSuccess(); err != nil { logrus.Infof("Adding %s to missing packages", pkg) missingPkgs = append(missingPkgs, pkg) } } if len(missingPkgs) > 0 { logrus.Warnf("The following packages are not installed via %s: %s", checker.manager, strings.Join(missingPkgs, ", ")) // TODO: `install` might not be the install command for every package // manager logrus.Infof("Install them with: sudo %s install %s", checker.manager, strings.Join(missingPkgs, " ")) return false, nil } return true, nil } /* ############################################################################# # Simple yes/no prompt # # @optparam default -n(default)/-y/-e (default to n, y or make (e)xplicit) # @param message common::askyorn () { local yorn local def=n local msg="y/N" case $1 in -y) # yes default def="y" msg="Y/n" shift ;; -e) # Explicit def="" msg="y/n" shift ;; -n) shift ;; esac while [[ $yorn != [yYnN] ]]; do logecho -n "$*? ($msg): " read yorn : ${yorn:=$def} done # Final test to set return code [[ $yorn == [yY] ]] } */ // readInput prints a question and then reads an answer from the user // // If the user presses Ctrl+C instead of answering, this funtcion will // return an error crafted with UserInputError. This error can be queried // to find out if the user canceled the input using its method IsCtrlC: // // if err.(util.UserInputError).IsCtrlC() {} // // Note that in case of cancelling input, the user will still have to press // enter to finish the scan. func readInput(question string) (string, error) { fmt.Print(question) // Trap Ctrl+C if a user wishes to cancel the input inputChannel := make(chan string, 1) signalChannel := make(chan os.Signal, 1) signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM) defer func() { signal.Stop(signalChannel) close(signalChannel) }() go func() { scanner := bufio.NewScanner(os.Stdin) scanner.Scan() response := scanner.Text() inputChannel <- response close(inputChannel) }() select { case <-signalChannel: return "", NewUserInputError("Input canceled", true) case response := <-inputChannel: return response, nil } } // Ask asks the user a question, expecting a known response expectedResponse // // You may specify a single response as a string or a series // of valid/invalid responses with an optional default. // // To specify the valid responses, either pass a string or craft a series // of answers using the following format: // // "|successAnswers|nonSuccessAnswers|defaultAnswer" // // The successAnswers and nonSuccessAnswers can be either a string or a // series os responses like: // // "|opt1a:opt1b|opt2a:opt2b|defaultAnswer" // // This example will accept opt1a and opt1b as successful answers, opt2a and // opt2b as unsuccessful answers and in case of an empty answer, it will // return "defaultAnswer" as success. // // To consider the default as a success, simply list them with the rest of the // non successful answers. func Ask(question, expectedResponse string, retries int) (answer string, success bool, err error) { attempts := 1 if retries < 0 { fmt.Printf("Retries was set to a number less than zero (%d). Please specify a positive number of retries or zero, if you want to ask unconditionally.\n", retries) } const ( partsSeparator string = "|" optsSeparator string = ":" ) successAnswers := make([]string, 0) nonSuccessAnswers := make([]string, 0) defaultAnswer := "" // Check out if string has several options if strings.Contains(expectedResponse, partsSeparator) { parts := strings.Split(expectedResponse, partsSeparator) if len(parts) > 3 { return "", false, errors.New("answer spec malformed") } // The first part has the answers to consider a success if strings.Contains(expectedResponse, parts[0]) { successAnswers = strings.Split(parts[0], optsSeparator) } // If there is a second part, its non success, but expected responses if len(parts) >= 2 { if strings.Contains(parts[1], optsSeparator) { nonSuccessAnswers = strings.Split(parts[1], optsSeparator) } else { nonSuccessAnswers = append(nonSuccessAnswers, parts[1]) } } // If we have a fourth part, its the default answer if len(parts) == 3 { defaultAnswer = parts[2] } } for attempts <= retries { // Read the input from the user answer, err = readInput(fmt.Sprintf("%s (%d/%d) \n", question, attempts, retries)) if err != nil { return answer, false, err } // if we have multiple options, use those and ignore the expected string if len(successAnswers) > 0 { // check the right answers for _, testResponse := range successAnswers { if answer == testResponse { return answer, true, nil } } // if we have wrong, but accepted answers, try those for _, testResponse := range nonSuccessAnswers { if answer == testResponse { return answer, false, nil } // If answer is the default, and it is a nonSuccess, return it if answer == "" && defaultAnswer == testResponse { return defaultAnswer, false, nil } } } else if answer == expectedResponse { return answer, true, nil } if answer == "" && defaultAnswer != "" { return defaultAnswer, true, nil } fmt.Printf("Expected '%s', but got '%s'\n", expectedResponse, answer) attempts++ } return answer, false, NewUserInputError("expected response was not input. Retries exceeded", false) } // MoreRecent determines if file at path a was modified more recently than file // at path b. If one file does not exist, the other will be treated as most // recent. If both files do not exist or an error occurs, an error is returned. func MoreRecent(a, b string) (bool, error) { fileA, errA := os.Stat(a) if errA != nil && !os.IsNotExist(errA) { return false, errA } fileB, errB := os.Stat(b) if errB != nil && !os.IsNotExist(errB) { return false, errB } switch { case os.IsNotExist(errA) && os.IsNotExist(errB): return false, errors.New("neither file exists") case os.IsNotExist(errA): return false, nil case os.IsNotExist(errB): return true, nil } return (fileA.ModTime().Unix() >= fileB.ModTime().Unix()), nil } func AddTagPrefix(tag string) string { if strings.HasPrefix(tag, TagPrefix) { return tag } return TagPrefix + tag } func TrimTagPrefix(tag string) string { return strings.TrimPrefix(tag, TagPrefix) } func TagStringToSemver(tag string) (semver.Version, error) { return semver.Make(TrimTagPrefix(tag)) } func SemverToTagString(tag semver.Version) string { return AddTagPrefix(tag.String()) } // CopyFileLocal copies a local file from one local location to another. func CopyFileLocal(src, dst string, required bool) error { logrus.Infof("Trying to copy file %s to %s (required: %v)", src, dst, required) srcStat, err := os.Stat(src) if err != nil && required { return fmt.Errorf("source %s is required but does not exist: %w", src, err) } if os.IsNotExist(err) && !required { logrus.Infof( "File %s does not exist but is also not required", filepath.Base(src), ) return nil } if !srcStat.Mode().IsRegular() { return errors.New("cannot copy non-regular file: IsRegular reports " + "whether m describes a regular file. That is, it tests that no " + "mode type bits are set") } source, err := os.Open(src) if err != nil { return fmt.Errorf("open source file %s: %w", src, err) } defer source.Close() destination, err := os.Create(dst) if err != nil { return fmt.Errorf("create destination file %s: %w", dst, err) } defer destination.Close() if _, err := io.Copy(destination, source); err != nil { return fmt.Errorf("copy source %s to destination %s: %w", src, dst, err) } logrus.Infof("Copied %s", filepath.Base(dst)) return nil } // CopyDirContentsLocal copies local directory contents from one local location // to another. func CopyDirContentsLocal(src, dst string) error { logrus.Infof("Trying to copy dir %s to %s", src, dst) // If initial destination does not exist create it. if _, err := os.Stat(dst); err != nil { if err := os.MkdirAll(dst, os.FileMode(0o755)); err != nil { return fmt.Errorf("create destination directory %s: %w", dst, err) } } files, err := os.ReadDir(src) if err != nil { return fmt.Errorf("reading source dir %s: %w", src, err) } for _, file := range files { srcPath := filepath.Join(src, file.Name()) dstPath := filepath.Join(dst, file.Name()) fileInfo, err := os.Stat(srcPath) if err != nil { return fmt.Errorf("stat source path %s: %w", srcPath, err) } switch fileInfo.Mode() & os.ModeType { case os.ModeDir: if !Exists(dstPath) { if err := os.MkdirAll(dstPath, os.FileMode(0o755)); err != nil { return fmt.Errorf("creating destination dir %s: %w", dstPath, err) } } if err := CopyDirContentsLocal(srcPath, dstPath); err != nil { return fmt.Errorf("copy %s to %s: %w", srcPath, dstPath, err) } default: if err := CopyFileLocal(srcPath, dstPath, false); err != nil { return fmt.Errorf("copy %s to %s: %w", srcPath, dstPath, err) } } } return nil } // RemoveAndReplaceDir removes a directory and its contents then recreates it. func RemoveAndReplaceDir(path string) error { logrus.Infof("Removing %s", path) if err := os.RemoveAll(path); err != nil { return fmt.Errorf("remove %s: %w", path, err) } logrus.Infof("Creating %s", path) if err := os.MkdirAll(path, os.FileMode(0o755)); err != nil { return fmt.Errorf("create %s: %w", path, err) } return nil } // Exists indicates whether a file exists. func Exists(path string) bool { if _, err := os.Stat(path); os.IsNotExist(err) { return false } return true } // IsDir returns true if the path is a directory. func IsDir(path string) bool { info, err := os.Stat(path) if err != nil { return false } if info.IsDir() { return true } return false } // WrapText wraps a text. func WrapText(originalText string, lineSize int) (wrappedText string) { words := strings.Fields(strings.TrimSpace(originalText)) wrappedText = words[0] spaceLeft := lineSize - len(wrappedText) for _, word := range words[1:] { if len(word)+1 > spaceLeft { wrappedText += "\n" + word spaceLeft = lineSize - len(word) } else { wrappedText += " " + word spaceLeft -= 1 + len(word) } } return wrappedText } // StripControlCharacters takes a slice of bytes and removes control // characters and bare line feeds (ported from the original bash anago). func StripControlCharacters(logData []byte) []byte { return regexpCRLF.ReplaceAllLiteral( regexpCtrlChar.ReplaceAllLiteral(logData, []byte{}), []byte{}, ) } // StripSensitiveData removes data deemed sensitive or non public // from a byte slice (ported from the original bash anago). func StripSensitiveData(logData []byte) []byte { // Remove OAuth tokens logData = regexpOauthToken.ReplaceAllLiteral(logData, []byte("__SANITIZED__:x-oauth-basic")) // Remove GitHub tokens logData = regexpGitToken.ReplaceAllLiteral(logData, []byte("//git:__SANITIZED__:@github.com")) return logData } // CleanLogFile cleans control characters and sensitive data from a file. func CleanLogFile(logPath string) (err error) { logrus.Debugf("Sanitizing logfile %s", logPath) // Open a tempfile to write sanitized log tempFile, err := os.CreateTemp("", "temp-release-log-") if err != nil { return fmt.Errorf("creating temp file for sanitizing log: %w", err) } defer func() { err = tempFile.Close() os.Remove(tempFile.Name()) }() // Open the new logfile for reading logFile, err := os.Open(logPath) if err != nil { return fmt.Errorf("while opening %s : %w", logPath, err) } // Scan the log and pass it through the cleaning funcs scanner := bufio.NewScanner(logFile) for scanner.Scan() { chunk := scanner.Bytes() chunk = StripControlCharacters( StripSensitiveData(chunk), ) chunk = append(chunk, []byte{10}...) _, err := tempFile.Write(chunk) if err != nil { return fmt.Errorf("while writing buffer to file: %w", err) } } if err := logFile.Close(); err != nil { return fmt.Errorf("closing log file: %w", err) } if err := CopyFileLocal(tempFile.Name(), logPath, true); err != nil { return fmt.Errorf("writing clean logfile: %w", err) } return err } release-utils-0.8.5/util/common_test.go000066400000000000000000000362131467053121000201410ustar00rootroot00000000000000/* Copyright 2019 The Kubernetes Authors. 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 util import ( "errors" "os" "path/filepath" "testing" "time" "github.com/blang/semver/v4" "github.com/stretchr/testify/require" ) func TestPackagesAvailableSuccess(t *testing.T) { testcases := [][]string{ {"bash"}, {"bash", "curl", "grep"}, {}, } for _, packages := range testcases { available, err := PackagesAvailable(packages...) require.NoError(t, err) require.True(t, available) } } func TestPackagesAvailableFailure(t *testing.T) { testcases := [][]string{ { "fakepackagefoo", }, { "fakepackagefoo", "fakepackagebar", "fakepackagebaz", }, { "bash", "fakepackagefoo", "fakepackagebar", }, } for _, packages := range testcases { actual, err := PackagesAvailable(packages...) require.NoError(t, err) require.False(t, actual) } } func TestMoreRecent(t *testing.T) { baseTmpDir, err := os.MkdirTemp("", "") require.NoError(t, err) // Create test files. testFileOne := filepath.Join(baseTmpDir, "testone.txt") require.NoError(t, os.WriteFile( testFileOne, []byte("file-one-contents"), os.FileMode(0o644), )) time.Sleep(1 * time.Second) testFileTwo := filepath.Join(baseTmpDir, "testtwo.txt") require.NoError(t, os.WriteFile( testFileTwo, []byte("file-two-contents"), os.FileMode(0o644), )) notFile := filepath.Join(baseTmpDir, "noexist.txt") defer cleanupTmp(t, baseTmpDir) type args struct { a string b string } type want struct { r bool err error } cases := map[string]struct { args args want want }{ "AIsRecent": { args: args{ a: testFileTwo, b: testFileOne, }, want: want{ r: true, err: nil, }, }, "AIsNotRecent": { args: args{ a: testFileOne, b: testFileTwo, }, want: want{ r: false, err: nil, }, }, "ADoesNotExist": { args: args{ a: notFile, b: testFileTwo, }, want: want{ r: false, err: nil, }, }, "BDoesNotExist": { args: args{ a: testFileOne, b: notFile, }, want: want{ r: true, err: nil, }, }, "NeitherExists": { args: args{ a: notFile, b: notFile, }, want: want{ r: false, err: errors.New("neither file exists"), }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { more, err := MoreRecent(tc.args.a, tc.args.b) require.IsType(t, tc.want.err, err) require.Equal(t, tc.want.r, more) }) } } func TestCopyFile(t *testing.T) { srcDir, err := os.MkdirTemp("", "src") require.NoError(t, err) dstDir, err := os.MkdirTemp("", "dst") require.NoError(t, err) // Create test file. srcFileOnePath := filepath.Join(srcDir, "testone.txt") require.NoError(t, os.WriteFile( srcFileOnePath, []byte("file-one-contents"), os.FileMode(0o644), )) dstFileOnePath := filepath.Join(dstDir, "testone.txt") defer cleanupTmp(t, srcDir) defer cleanupTmp(t, dstDir) type args struct { src string dst string required bool } cases := map[string]struct { args args shouldError bool }{ "CopyFileSuccess": { args: args{ src: srcFileOnePath, dst: dstFileOnePath, required: true, }, shouldError: false, }, "CopyFileNotExistNotIgnore": { args: args{ src: "path/does/not/exit", dst: dstFileOnePath, required: true, }, shouldError: true, }, "CopyFileNotExistIgnore": { args: args{ src: "path/does/not/exit", dst: dstFileOnePath, required: false, }, shouldError: false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { copyErr := CopyFileLocal(tc.args.src, tc.args.dst, tc.args.required) if tc.shouldError { require.Error(t, copyErr) } else { require.NoError(t, copyErr) } if copyErr == nil { _, err := os.Stat(tc.args.dst) if err != nil && tc.args.required { t.Fatal("file does not exist in destination") } } }) } } func TestCopyDirContentLocal(t *testing.T) { srcDir, err := os.MkdirTemp("", "src") require.NoError(t, err) dstDir, err := os.MkdirTemp("", "dst") require.NoError(t, err) // Create test file. srcFileOnePath := filepath.Join(srcDir, "testone.txt") require.NoError(t, os.WriteFile( srcFileOnePath, []byte("file-one-contents"), os.FileMode(0o644), )) srcFileTwoPath := filepath.Join(srcDir, "testtwo.txt") require.NoError(t, os.WriteFile( srcFileTwoPath, []byte("file-two-contents"), os.FileMode(0o644), )) defer cleanupTmp(t, srcDir) defer cleanupTmp(t, dstDir) type args struct { src string dst string } type want struct { err error } cases := map[string]struct { args args want want }{ "CopyDirContentsSuccess": { args: args{ src: srcDir, dst: dstDir, }, want: want{ err: nil, }, }, "CopyDirContentsSuccessDstNotExist": { args: args{ src: srcDir, dst: filepath.Join(dstDir, "path-not-exist"), }, want: want{ err: nil, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { copyErr := CopyDirContentsLocal(tc.args.src, tc.args.dst) require.Equal(t, tc.want.err, copyErr) }) } } func TestRemoveAndReplaceDir(t *testing.T) { dir, err := os.MkdirTemp("", "rm") require.NoError(t, err) // Create test file. fileOnePath := filepath.Join(dir, "testone.txt") require.NoError(t, os.WriteFile( fileOnePath, []byte("file-one-contents"), os.FileMode(0o644), )) fileTwoPath := filepath.Join(dir, "testtwo.txt") require.NoError(t, os.WriteFile( fileTwoPath, []byte("file-two-contents"), os.FileMode(0o644), )) defer cleanupTmp(t, dir) type args struct { dir string } type want struct { err error } cases := map[string]struct { args args want want }{ "RemoveAndReplaceSuccess": { args: args{ dir: dir, }, want: want{ err: nil, }, }, "RemoveAndReplaceNotExist": { args: args{ dir: filepath.Join(dir, "not-exit"), }, want: want{ err: nil, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { err := RemoveAndReplaceDir(tc.args.dir) require.Equal(t, tc.want.err, err) }) } } func TestExist(t *testing.T) { dir, err := os.MkdirTemp("", "rm") require.NoError(t, err) // Create test file. fileOnePath := filepath.Join(dir, "testone.txt") require.NoError(t, os.WriteFile( fileOnePath, []byte("file-one-contents"), os.FileMode(0o644), )) defer cleanupTmp(t, dir) type args struct { dir string } type want struct { exist bool } cases := map[string]struct { args args want want }{ "DirExists": { args: args{ dir: dir, }, want: want{ exist: true, }, }, "FileExists": { args: args{ dir: fileOnePath, }, want: want{ exist: true, }, }, "DirNotExists": { args: args{ dir: filepath.Join(dir, "path-not-exit"), }, want: want{ exist: false, }, }, "FileNotExists": { args: args{ dir: filepath.Join(dir, "notexist.txt"), }, want: want{ exist: false, }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { exist := Exists(tc.args.dir) require.Equal(t, tc.want.exist, exist) }) } } func cleanupTmp(t *testing.T, dir string) { require.NoError(t, os.RemoveAll(dir)) } func TestTagStringToSemver(t *testing.T) { // Success version, err := TagStringToSemver("v1.2.3") require.NoError(t, err) require.Equal(t, semver.Version{Major: 1, Minor: 2, Patch: 3}, version) // No Major.Minor.Patch elements found version, err = TagStringToSemver("invalid") require.Error(t, err) require.Equal(t, semver.Version{}, version) // Version string empty version, err = TagStringToSemver("") require.Error(t, err) require.Equal(t, semver.Version{}, version) } func TestSemverToTagString(t *testing.T) { version := semver.Version{Major: 1, Minor: 2, Patch: 3} require.Equal(t, "v1.2.3", SemverToTagString(version)) } func TestAddTagPrefix(t *testing.T) { require.Equal(t, "v0.0.0", AddTagPrefix("0.0.0")) require.Equal(t, "v1.0.0", AddTagPrefix("v1.0.0")) } func TestTrimTagPrefix(t *testing.T) { require.Equal(t, "0.0.0", TrimTagPrefix("0.0.0")) require.Equal(t, "1.0.0", TrimTagPrefix("1.0.0")) } func TestWrapText(t *testing.T) { //nolint: misspell longText := `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut molestie accumsan orci, id congue nibh sollicitudin in. Nulla condimentum arcu eu est hendrerit tempus. Nunc risus nibh, aliquam in ultrices fringilla, aliquet ac purus. Aenean non nibh magna. Nunc lacinia suscipit malesuada. Vivamus porta a leo vel ornare. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Morbi pellentesque orci magna, sed semper nulla fringilla at. Nam elementum ipsum maximus lectus tempor faucibus. Donec eu enim nulla. Integer egestas venenatis tristique. Curabitur id purus sem. Vivamus nec mollis lorem.` wrappedText := "Lorem ipsum dolor sit amet, consectetur\n" wrappedText += "adipiscing elit. Ut molestie accumsan\n" wrappedText += "orci, id congue nibh sollicitudin in.\n" wrappedText += "Nulla condimentum arcu eu est hendrerit\n" wrappedText += "tempus. Nunc risus nibh, aliquam in\n" wrappedText += "ultrices fringilla, aliquet ac purus.\n" wrappedText += "Aenean non nibh magna. Nunc lacinia\n" wrappedText += "suscipit malesuada. Vivamus porta a leo\n" wrappedText += "vel ornare. Orci varius natoque\n" wrappedText += "penatibus et magnis dis parturient\n" wrappedText += "montes, nascetur ridiculus mus. Morbi\n" //nolint: misspell wrappedText += "pellentesque orci magna, sed semper\n" wrappedText += "nulla fringilla at. Nam elementum ipsum\n" wrappedText += "maximus lectus tempor faucibus. Donec eu\n" wrappedText += "enim nulla. Integer egestas venenatis\n" wrappedText += "tristique. Curabitur id purus sem.\n" wrappedText += "Vivamus nec mollis lorem." require.Equal(t, WrapText(longText, 40), wrappedText) } func TestStripSensitiveData(t *testing.T) { testCases := []struct { text string mustChange bool }{ {text: "a", mustChange: false}, {text: `s;!3Vc2]x~qL&'Sc/W/>^}8pau\.xr;;5uL:mL:h:x-oauth-basic`, mustChange: false}, // Non base64 token {text: `ab0ff5efdbafcf1def98cac7bd4fa5856d53d000:x-oauth-basic`, mustChange: true}, // Visible token {text: `X-Some-Header: ab0ff5efdbafcf1def98cac7bd4fa5856d53d000:x-oauth-basic;`, mustChange: true}, // in string {text: `error: failed to push some refs to 'https://git:538b8ca9618eaf316b8ca37bcf78da2c24639c14@github.com/kubernetes/kubernetes.git'`, mustChange: true}, // GitHub token {text: `error: failed to push some refs to 'https://git:538b8c9618a316bca3bcf78da2c24639c35@github.com/kubernetes/kubernetes.git'`, mustChange: true}, // 35-char GitHub token } for _, tc := range testCases { testBytes := []byte(tc.text) if tc.mustChange { require.NotEqual(t, StripSensitiveData(testBytes), testBytes, "Failed sanitizing "+tc.text) } else { require.ElementsMatch(t, StripSensitiveData(testBytes), testBytes) } } } func TestStripControlCharacters(t *testing.T) { testCases := []struct { text []byte mustChange bool }{ {text: append([]byte{27}, []byte("[1m")...), mustChange: true}, {text: append([]byte{27}, []byte("[1K")...), mustChange: true}, {text: append([]byte{27}, []byte("[1B")...), mustChange: true}, {text: append([]byte{27}, []byte("(1B")...), mustChange: true}, // Parenthesis {text: append([]byte{27}, []byte("[1;1m")...), mustChange: true}, // ; + 1 digit {text: append([]byte{27}, []byte("[1;12m")...), mustChange: true}, // ; + 2 digits {text: append([]byte{27}, []byte("[21K")...), mustChange: true}, // {text: append([]byte{}, []byte("[1;13m")...), mustChange: false}, // No ESC {text: append([]byte{27}, []byte("[1,13m")...), mustChange: false}, // No semicolon {text: append([]byte("Test line"), []byte{13}...), mustChange: true}, // Bare CR {text: append([]byte("Test line"), []byte{13, 15}...), mustChange: false}, // CRLF {text: []byte("Test line"), mustChange: false}, // Plain string } for _, tc := range testCases { if tc.mustChange { require.NotEqual(t, StripControlCharacters(tc.text), tc.text) } else { require.ElementsMatch(t, StripControlCharacters(tc.text), tc.text) } } } func TestCleanLogFile(t *testing.T) { line1 := "This is a test log\n" line2 := "It should not contain a test token here:\n" line3 := "nor control characters o bare line feeds here:\n" line4 := "Bare line feed: " line5 := "\nControl Chars: " // Create a token line originalTokenLine := "7aa33bd2186c40849c4c2df321241e241def98ca:x-oauth-basic" //nolint: gosec sanitizedTokenLine := string(StripSensitiveData([]byte(originalTokenLine))) require.NotEqual(t, originalTokenLine, sanitizedTokenLine) // Create the log originalLog := line1 + line2 + originalTokenLine + line3 + line4 + string([]byte{13}) + line5 + string(append([]byte{27}, []byte("[1;1m")...)) + "\n" // And expected output cleanLog := line1 + line2 + sanitizedTokenLine + line3 + line4 + line5 + "\n" logfile, err := os.CreateTemp("", "clean-log-test-") require.NoError(t, err, "creating test logfile") defer os.Remove(logfile.Name()) err = os.WriteFile(logfile.Name(), []byte(originalLog), os.FileMode(0o644)) require.NoError(t, err, "writing test file") // Now, run the cleanLogFile err = CleanLogFile(logfile.Name()) require.NoError(t, err, "running log cleaner") resultingData, err := os.ReadFile(logfile.Name()) require.NoError(t, err, "reading modified file") require.NotEmpty(t, resultingData) // Must have changed require.NotEqual(t, originalLog, string(resultingData)) require.Equal(t, cleanLog, string(resultingData)) } func TestIsDir(t *testing.T) { t.Parallel() for _, tc := range []struct { name string prepare func(t *testing.T) string expected bool }{ { name: "isdir", prepare: func(t *testing.T) string { t.Helper() dir := t.TempDir() return dir }, expected: true, }, { name: "isfile", prepare: func(t *testing.T) string { t.Helper() dir := t.TempDir() path := filepath.Join(dir, "file.txt") require.NoError(t, os.WriteFile(path, []byte("Yo!"), os.FileMode(0o644))) return path }, expected: false, }, { name: "nonexisting", prepare: func(t *testing.T) string { t.Helper() dir := t.TempDir() path := filepath.Join(dir, "not-there.txt") return path }, expected: false, }, } { t.Run(tc.name, func(t *testing.T) { t.Parallel() path := tc.prepare(t) require.Equal(t, tc.expected, IsDir(path)) }) } } release-utils-0.8.5/version/000077500000000000000000000000001467053121000157665ustar00rootroot00000000000000release-utils-0.8.5/version/command.go000066400000000000000000000034751467053121000177440ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. 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 import ( "fmt" "github.com/spf13/cobra" ) // Version returns a cobra command to be added to another cobra command, like: // ```go // // rootCmd.AddCommand(version.Version()) // // ```. func Version() *cobra.Command { return version("") } // WithFont returns a cobra command to be added to another cobra command with a select font for ASCII, like: // ```go // // rootCmd.AddCommand(version.WithFont("starwars")) // // ```. func WithFont(fontName string) *cobra.Command { return version(fontName) } func version(fontName string) *cobra.Command { var outputJSON bool cmd := &cobra.Command{ Use: "version", Short: "Prints the version", RunE: func(cmd *cobra.Command, _ []string) error { v := GetVersionInfo() v.Name = cmd.Root().Name() v.Description = cmd.Root().Short v.FontName = "" if fontName != "" && v.CheckFontName(fontName) { v.FontName = fontName } cmd.SetOut(cmd.OutOrStdout()) if outputJSON { out, err := v.JSONString() if err != nil { return fmt.Errorf("unable to generate JSON from version info: %w", err) } cmd.Println(out) } else { cmd.Println(v.String()) } return nil }, } cmd.Flags().BoolVar(&outputJSON, "json", false, "print JSON instead of text") return cmd } release-utils-0.8.5/version/command_test.go000066400000000000000000000020571467053121000207760ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. 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_test import ( "testing" "sigs.k8s.io/release-utils/version" ) func TestVersion(t *testing.T) { v := version.Version() err := v.Execute() if err != nil { t.Errorf("%v", err) } } func TestVersionWithFont(t *testing.T) { v := version.WithFont("fender") err := v.Execute() if err != nil { t.Errorf("%v", err) } } func TestVersionJson(t *testing.T) { v := version.Version() v.SetArgs([]string{"--json"}) err := v.Execute() if err != nil { t.Errorf("%v", err) } } release-utils-0.8.5/version/doc.go000066400000000000000000000024751467053121000170720ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. 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 provides an importable cobra command and a fixed package // path location to set compile time version information. To override the // default values, set the `-ldflags` flags with the following strings: // // sigs.k8s.io/release-utils/version.gitVersion= // sigs.k8s.io/release-utils/version.gitCommit= // sigs.k8s.io/release-utils/version.gitTreeState= // sigs.k8s.io/release-utils/version.buildDate= // // Example: `go build -ldflags " -X sigs.k8s.io/release-utils/version.gitVersion=v0.4.0-1-g040f53c -X sigs.k8s.io/release-utils/version.gitCommit=040f53c -X sigs.k8s.io/release-utils/version.gitTreeState=dirty -X sigs.k8s.io/release-utils/version.buildDate=2022-02-03T17:30:01Z" .` package version release-utils-0.8.5/version/version.go000066400000000000000000000125001467053121000200000ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. 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 import ( "encoding/json" "fmt" "os" "runtime" "runtime/debug" "strings" "sync" "text/tabwriter" "time" "github.com/common-nighthawk/go-figure" ) const unknown = "unknown" // Base version information. // // This is the fallback data used when version information from git is not // provided via go ldflags. var ( // Output of "git describe". The prerequisite is that the // branch should be tagged using the correct versioning strategy. gitVersion = "devel" // SHA1 from git, output of $(git rev-parse HEAD). gitCommit = unknown // State of git tree, either "clean" or "dirty". gitTreeState = unknown // Build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ'). buildDate = unknown // flag to print the ascii name banner. asciiName = "true" // goVersion is the used golang version. goVersion = unknown // compiler is the used golang compiler. compiler = unknown // platform is the used os/arch identifier. platform = unknown once sync.Once info = Info{} ) type Info struct { GitVersion string `json:"gitVersion"` GitCommit string `json:"gitCommit"` GitTreeState string `json:"gitTreeState"` BuildDate string `json:"buildDate"` GoVersion string `json:"goVersion"` Compiler string `json:"compiler"` Platform string `json:"platform"` ASCIIName string `json:"-"` FontName string `json:"-"` Name string `json:"-"` Description string `json:"-"` } func getBuildInfo() *debug.BuildInfo { bi, ok := debug.ReadBuildInfo() if !ok { return nil } return bi } func getGitVersion(bi *debug.BuildInfo) string { if bi == nil { return unknown } // TODO: remove this when the issue https://github.com/golang/go/issues/29228 is fixed if bi.Main.Version == "(devel)" || bi.Main.Version == "" { return gitVersion } return bi.Main.Version } func getCommit(bi *debug.BuildInfo) string { return getKey(bi, "vcs.revision") } func getDirty(bi *debug.BuildInfo) string { modified := getKey(bi, "vcs.modified") if modified == "true" { return "dirty" } if modified == "false" { return "clean" } return unknown } func getBuildDate(bi *debug.BuildInfo) string { buildTime := getKey(bi, "vcs.time") t, err := time.Parse("2006-01-02T15:04:05Z", buildTime) if err != nil { return unknown } return t.Format("2006-01-02T15:04:05") } func getKey(bi *debug.BuildInfo, key string) string { if bi == nil { return unknown } for _, iter := range bi.Settings { if iter.Key == key { return iter.Value } } return unknown } // GetVersionInfo represents known information on how this binary was built. func GetVersionInfo() Info { once.Do(func() { buildInfo := getBuildInfo() gitVersion = getGitVersion(buildInfo) if gitCommit == unknown { gitCommit = getCommit(buildInfo) } if gitTreeState == unknown { gitTreeState = getDirty(buildInfo) } if buildDate == unknown { buildDate = getBuildDate(buildInfo) } if goVersion == unknown { goVersion = runtime.Version() } if compiler == unknown { compiler = runtime.Compiler } if platform == unknown { platform = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) } info = Info{ ASCIIName: asciiName, GitVersion: gitVersion, GitCommit: gitCommit, GitTreeState: gitTreeState, BuildDate: buildDate, GoVersion: goVersion, Compiler: compiler, Platform: platform, } }) return info } // String returns the string representation of the version info. func (i *Info) String() string { b := strings.Builder{} w := tabwriter.NewWriter(&b, 0, 0, 2, ' ', 0) // name and description are optional. if i.Name != "" { if i.ASCIIName == "true" { f := figure.NewFigure(strings.ToUpper(i.Name), i.FontName, true) _, _ = fmt.Fprint(w, f.String()) } _, _ = fmt.Fprint(w, i.Name) if i.Description != "" { _, _ = fmt.Fprintf(w, ": %s", i.Description) } _, _ = fmt.Fprint(w, "\n\n") } _, _ = fmt.Fprintf(w, "GitVersion:\t%s\n", i.GitVersion) _, _ = fmt.Fprintf(w, "GitCommit:\t%s\n", i.GitCommit) _, _ = fmt.Fprintf(w, "GitTreeState:\t%s\n", i.GitTreeState) _, _ = fmt.Fprintf(w, "BuildDate:\t%s\n", i.BuildDate) _, _ = fmt.Fprintf(w, "GoVersion:\t%s\n", i.GoVersion) _, _ = fmt.Fprintf(w, "Compiler:\t%s\n", i.Compiler) _, _ = fmt.Fprintf(w, "Platform:\t%s\n", i.Platform) _ = w.Flush() return b.String() } // JSONString returns the JSON representation of the version info. func (i *Info) JSONString() (string, error) { b, err := json.MarshalIndent(i, "", " ") if err != nil { return "", err } return string(b), nil } func (i *Info) CheckFontName(fontName string) bool { assetNames := figure.AssetNames() for _, font := range assetNames { if strings.Contains(font, fontName) { return true } } fmt.Fprintln(os.Stderr, "font not valid, using default") return false } release-utils-0.8.5/version/version_test.go000066400000000000000000000016031467053121000210410ustar00rootroot00000000000000/* Copyright 2022 The Kubernetes Authors. 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 import ( "testing" "github.com/stretchr/testify/require" ) func TestVersionText(t *testing.T) { sut := GetVersionInfo() require.NotEmpty(t, sut.String()) } func TestVersionJSON(t *testing.T) { sut := GetVersionInfo() json, err := sut.JSONString() require.NoError(t, err) require.NotEmpty(t, json) }