├── helpers ├── testdata │ ├── empty_table.golden │ ├── no_options.golden │ ├── with_header_option.golden │ ├── with_single_option.golden │ ├── with_multiple_options.golden │ ├── with_footer_option.golden │ └── multiple_rows.golden ├── doc.go ├── tablewriter.go └── tablewriter_test.go ├── .bom.yaml ├── code-of-conduct.md ├── Dockerfile.dev ├── README.md ├── OWNERS ├── scripts ├── boilerplate │ ├── boilerplate.goheader.txt │ ├── boilerplate.generatego.txt │ ├── boilerplate.go.txt │ ├── boilerplate.py.txt │ ├── boilerplate.sh.txt │ ├── boilerplate.Makefile.txt │ └── boilerplate.Dockerfile.txt └── verify-build.sh ├── SECURITY_CONTACTS ├── mage.go ├── .github ├── dependabot.yml ├── workflows │ └── release.yaml └── PULL_REQUEST_TEMPLATE.md ├── internal └── tools │ └── tools.go ├── command ├── global_test.go ├── global.go ├── command_test.go └── command.go ├── mage ├── version_test.go ├── cosign.go ├── ko.go ├── git.go ├── version.go ├── dependency.go ├── boilerplate.go └── golangci-lint.go ├── OWNERS_ALIASES ├── version ├── version_test.go ├── command_test.go ├── doc.go ├── command.go └── version.go ├── SECURITY.md ├── env ├── env.go ├── internal │ ├── impl.go │ └── internalfakes │ │ └── fake_impl.go └── env_test.go ├── http ├── http.go ├── example_multi_test.go ├── doc.go ├── agent_test.go ├── httpfakes │ └── fake_agent_implementation.go └── http_test.go ├── log ├── log_test.go ├── step.go ├── log.go └── hooks.go ├── dependencies.yaml ├── go.mod ├── CONTRIBUTING.md ├── hash ├── hash.go └── hash_test.go ├── magefile.go ├── editor ├── editor_test.go ├── tty.go └── editor.go ├── .gitignore ├── .golangci.yml ├── tar ├── tar_test.go └── tar.go ├── go.sum └── LICENSE /helpers/testdata/empty_table.golden: -------------------------------------------------------------------------------- 1 | ┌──────┬─────┐ 2 | │ NAME │ AGE │ 3 | └──────┴─────┘ 4 | -------------------------------------------------------------------------------- /.bom.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | license: Apache-2.0 3 | name: sigs.k8s.io/release-utils 4 | creator: 5 | person: The Kubernetes Authors 6 | -------------------------------------------------------------------------------- /helpers/testdata/no_options.golden: -------------------------------------------------------------------------------- 1 | ┌──────┬─────┐ 2 | │ NAME │ AGE │ 3 | ├──────┼─────┤ 4 | │ John │ 30 │ 5 | └──────┴─────┘ 6 | -------------------------------------------------------------------------------- /helpers/testdata/with_header_option.golden: -------------------------------------------------------------------------------- 1 | ┌──────┬─────┐ 2 | │ NAME │ AGE │ 3 | ├──────┼─────┤ 4 | │ John │ 30 │ 5 | └──────┴─────┘ 6 | -------------------------------------------------------------------------------- /helpers/testdata/with_single_option.golden: -------------------------------------------------------------------------------- 1 | ┌──────┬─────┐ 2 | │ NAME │ AGE │ 3 | ├──────┼─────┤ 4 | │ John │ 30 │ 5 | └──────┴─────┘ 6 | -------------------------------------------------------------------------------- /helpers/testdata/with_multiple_options.golden: -------------------------------------------------------------------------------- 1 | ┌──────┬─────┐ 2 | │ NAME │ AGE │ 3 | ├──────┼─────┤ 4 | │ John │ 30 │ 5 | └──────┴─────┘ 6 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Community Code of Conduct 2 | 3 | Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) 4 | -------------------------------------------------------------------------------- /helpers/testdata/with_footer_option.golden: -------------------------------------------------------------------------------- 1 | ┌───────┬─────┐ 2 | │ NAME │ AGE │ 3 | ├───────┼─────┤ 4 | │ John │ 30 │ 5 | ├───────┼─────┤ 6 | │ Total │ 1 │ 7 | └───────┴─────┘ 8 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | # This is used to we scrap the go version and use in CI to get the latest go version 2 | # and we use dependabot to keep the go version up to date 3 | FROM golang:1.25.5 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # release-utils 2 | 3 | Tiny utilities for use by the Release Engineering subproject and 4 | [kubernetes/release](https://github.com/kubernetes/release/). 5 | 6 | Hopefully they can be useful to you too! 7 | -------------------------------------------------------------------------------- /helpers/testdata/multiple_rows.golden: -------------------------------------------------------------------------------- 1 | ┌──────┬─────┬──────────┐ 2 | │ NAME │ AGE │ CITY │ 3 | ├──────┼─────┼──────────┤ 4 | │ John │ 30 │ New York │ 5 | │ Jane │ 25 │ Boston │ 6 | │ Bob │ 35 │ Chicago │ 7 | └──────┴─────┴──────────┘ 8 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | # See the OWNERS docs at https://go.k8s.io/owners 2 | 3 | approvers: 4 | - sig-release-leads 5 | - release-engineering-approvers 6 | reviewers: 7 | - release-engineering-approvers 8 | - release-engineering-reviewers 9 | labels: 10 | - sig/release 11 | - area/release-eng 12 | -------------------------------------------------------------------------------- /scripts/boilerplate/boilerplate.goheader.txt: -------------------------------------------------------------------------------- 1 | {{copyright-holder}} 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /SECURITY_CONTACTS: -------------------------------------------------------------------------------- 1 | # Defined below are the security contacts for this repo. 2 | # 3 | # They are the contact point for the Product Security Committee to reach out 4 | # to for triaging and handling of incoming issues. 5 | # 6 | # The below names agree to abide by the 7 | # [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy) 8 | # and will be removed and replaced if they violate that agreement. 9 | # 10 | # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE 11 | # INSTRUCTIONS AT https://kubernetes.io/security/ 12 | 13 | hasheddan 14 | jeremyrickard 15 | justaugustus 16 | saschagrunert 17 | -------------------------------------------------------------------------------- /helpers/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package helpers 18 | -------------------------------------------------------------------------------- /scripts/boilerplate/boilerplate.generatego.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | -------------------------------------------------------------------------------- /scripts/boilerplate/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright YEAR The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | -------------------------------------------------------------------------------- /scripts/boilerplate/boilerplate.py.txt: -------------------------------------------------------------------------------- 1 | # Copyright YEAR The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /scripts/boilerplate/boilerplate.sh.txt: -------------------------------------------------------------------------------- 1 | # Copyright YEAR The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /scripts/boilerplate/boilerplate.Makefile.txt: -------------------------------------------------------------------------------- 1 | # Copyright YEAR The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /scripts/boilerplate/boilerplate.Dockerfile.txt: -------------------------------------------------------------------------------- 1 | # Copyright YEAR The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /mage.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | /* 4 | Copyright 2021 The Kubernetes Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package main 20 | 21 | import ( 22 | "os" 23 | 24 | "github.com/magefile/mage/mage" 25 | ) 26 | 27 | func main() { os.Exit(mage.Main()) } 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | labels: 8 | - "area/dependency" 9 | - "release-note-none" 10 | - "ok-to-test" 11 | open-pull-requests-limit: 10 12 | groups: 13 | all: 14 | update-types: 15 | - "minor" 16 | - "patch" 17 | 18 | - package-ecosystem: "github-actions" 19 | directory: "/" 20 | schedule: 21 | interval: "weekly" 22 | open-pull-requests-limit: 10 23 | groups: 24 | actions: 25 | update-types: 26 | - "minor" 27 | - "patch" 28 | 29 | - package-ecosystem: "docker" 30 | directory: "/" 31 | schedule: 32 | interval: "weekly" 33 | groups: 34 | all: 35 | update-types: 36 | - "minor" 37 | - "patch" 38 | -------------------------------------------------------------------------------- /internal/tools/tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | /* 4 | Copyright 2021 The Kubernetes Authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // This is used to import things required by build scripts, to force `go mod` to see them as dependencies 20 | 21 | package internal 22 | 23 | import ( 24 | _ "github.com/maxbrunsfeld/counterfeiter/v6" 25 | ) 26 | -------------------------------------------------------------------------------- /command/global_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package command 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/require" 23 | ) 24 | 25 | func TestSetGlobalVerboseSuccess(t *testing.T) { 26 | require.False(t, GetGlobalVerbose()) 27 | SetGlobalVerbose(true) 28 | require.True(t, GetGlobalVerbose()) 29 | } 30 | -------------------------------------------------------------------------------- /mage/version_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package mage 18 | 19 | import "testing" 20 | 21 | func TestGenerateLDFlags(t *testing.T) { 22 | got, err := GenerateLDFlags() 23 | if err != nil { 24 | t.Errorf("failed to generate ld flags: %v", err) 25 | } 26 | 27 | if got == "" { 28 | t.Errorf("GenerateLDFlags() failed to return a string") 29 | } 30 | 31 | t.Log(got) 32 | } 33 | -------------------------------------------------------------------------------- /OWNERS_ALIASES: -------------------------------------------------------------------------------- 1 | # See the OWNERS docs at https://go.k8s.io/owners 2 | 3 | aliases: 4 | sig-release-leads: 5 | - cpanato # SIG Technical Lead 6 | - jeremyrickard # SIG Chair 7 | - justaugustus # SIG Chair 8 | - puerco # SIG Technical Lead 9 | - saschagrunert # SIG Chair 10 | - Verolop # SIG Technical Lead 11 | release-engineering-approvers: 12 | - cpanato # subproject owner / Release Manager 13 | - jeremyrickard # subproject owner / Release Manager 14 | - justaugustus # subproject owner / Release Manager 15 | - palnabarun # Release Manager 16 | - puerco # subproject owner / Release Manager 17 | - saschagrunert # subproject owner / Release Manager 18 | - xmudrii # Release Manager 19 | - Verolop # subproject owner / Release Manager 20 | release-engineering-reviewers: 21 | - ameukam # Release Manager Associate 22 | - cici37 # Release Manager Associate 23 | - jimangel # Release Manager Associate 24 | - jrsapi # Release Manager Associate 25 | - salaxander # Release Manager Associate 26 | -------------------------------------------------------------------------------- /version/version_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package version 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/require" 23 | ) 24 | 25 | func TestVersionText(t *testing.T) { 26 | sut := GetVersionInfo() 27 | require.NotEmpty(t, sut.String()) 28 | } 29 | 30 | func TestVersionJSON(t *testing.T) { 31 | sut := GetVersionInfo() 32 | json, err := sut.JSONString() 33 | 34 | require.NoError(t, err) 35 | require.NotEmpty(t, json) 36 | } 37 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Security Announcements 4 | 5 | Join the [kubernetes-security-announce] group for security and vulnerability announcements. 6 | 7 | You can also subscribe to an RSS feed of the above using [this link][kubernetes-security-announce-rss]. 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Instructions for reporting a vulnerability can be found on the 12 | [Kubernetes Security and Disclosure Information] page. 13 | 14 | ## Supported Versions 15 | 16 | Information about supported Kubernetes versions can be found on the 17 | [Kubernetes version and version skew support policy] page on the Kubernetes website. 18 | 19 | [kubernetes-security-announce]: https://groups.google.com/forum/#!forum/kubernetes-security-announce 20 | [kubernetes-security-announce-rss]: https://groups.google.com/forum/feed/kubernetes-security-announce/msgs/rss_v2_0.xml?num=50 21 | [Kubernetes version and version skew support policy]: https://kubernetes.io/docs/setup/release/version-skew-policy/#supported-versions 22 | [Kubernetes Security and Disclosure Information]: https://kubernetes.io/docs/reference/issues-security/security/#report-a-vulnerability 23 | -------------------------------------------------------------------------------- /env/env.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package env 18 | 19 | import ( 20 | "sigs.k8s.io/release-utils/env/internal" 21 | ) 22 | 23 | // Default returns either the provided environment variable for the given key 24 | // or the default value def if not set. 25 | func Default(key, def string) string { 26 | value, ok := internal.Impl.LookupEnv(key) 27 | if !ok || value == "" { 28 | return def 29 | } 30 | 31 | return value 32 | } 33 | 34 | // IsSet returns true if an environment variable is set. 35 | func IsSet(key string) bool { 36 | _, ok := internal.Impl.LookupEnv(key) 37 | 38 | return ok 39 | } 40 | -------------------------------------------------------------------------------- /http/http.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package http 18 | 19 | import ( 20 | "bytes" 21 | ) 22 | 23 | // GetURLResponse performs a get request and returns the response contents as a 24 | // string if successful. 25 | // 26 | // Deprecated: Use http.Agent.Get() instead. This function will be removed in a 27 | // future version of this package. 28 | func GetURLResponse(url string, trim bool) (string, error) { 29 | resp, err := NewAgent().Get(url) 30 | if err != nil { 31 | return "", err 32 | } 33 | 34 | if trim { 35 | resp = bytes.TrimSpace(resp) 36 | } 37 | 38 | return string(resp), nil 39 | } 40 | -------------------------------------------------------------------------------- /scripts/verify-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2021 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | PLATFORMS=( 22 | linux/amd64 23 | linux/386 24 | linux/arm 25 | linux/arm64 26 | linux/ppc64le 27 | linux/s390x 28 | windows/amd64 29 | windows/386 30 | freebsd/amd64 31 | darwin/amd64 32 | ) 33 | 34 | for PLATFORM in "${PLATFORMS[@]}"; do 35 | OS="${PLATFORM%/*}" 36 | ARCH=$(basename "$PLATFORM") 37 | 38 | echo "Building project for $PLATFORM" 39 | GOARCH="$ARCH" GOOS="$OS" go build ./... 40 | done 41 | -------------------------------------------------------------------------------- /env/internal/impl.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package internal 18 | 19 | import "os" 20 | 21 | //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate 22 | //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" 23 | 24 | //counterfeiter:generate . impl 25 | type impl interface { 26 | LookupEnv(key string) (string, bool) 27 | } 28 | 29 | type defImpl struct{} 30 | 31 | var Impl impl = &defImpl{} 32 | 33 | func (defImpl) LookupEnv(key string) (string, bool) { 34 | return os.LookupEnv(key) 35 | } 36 | -------------------------------------------------------------------------------- /command/global.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package command 18 | 19 | import ( 20 | "sync/atomic" 21 | ) 22 | 23 | // atomicInt is the global variable for storing the globally set verbosity 24 | // level. It should never be used directly to avoid data races. 25 | var atomicInt int32 26 | 27 | // SetGlobalVerbose sets the global command verbosity to the specified value. 28 | func SetGlobalVerbose(to bool) { 29 | var i int32 30 | if to { 31 | i = 1 32 | } 33 | 34 | atomic.StoreInt32(&atomicInt, i) 35 | } 36 | 37 | // GetGlobalVerbose returns the globally set command verbosity. 38 | func GetGlobalVerbose() bool { 39 | return atomic.LoadInt32(&atomicInt) != 0 40 | } 41 | -------------------------------------------------------------------------------- /version/command_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package version_test 18 | 19 | import ( 20 | "testing" 21 | 22 | "sigs.k8s.io/release-utils/version" 23 | ) 24 | 25 | func TestVersion(t *testing.T) { 26 | v := version.Version() 27 | 28 | err := v.Execute() 29 | if err != nil { 30 | t.Errorf("%v", err) 31 | } 32 | } 33 | 34 | func TestVersionWithFont(t *testing.T) { 35 | v := version.WithFont("fender") 36 | 37 | err := v.Execute() 38 | if err != nil { 39 | t.Errorf("%v", err) 40 | } 41 | } 42 | 43 | func TestVersionJson(t *testing.T) { 44 | v := version.Version() 45 | v.SetArgs([]string{"--json"}) 46 | 47 | err := v.Execute() 48 | if err != nil { 49 | t.Errorf("%v", err) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /log/log_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package log_test 18 | 19 | import ( 20 | "os" 21 | "testing" 22 | 23 | "github.com/sirupsen/logrus" 24 | "github.com/stretchr/testify/require" 25 | 26 | "sigs.k8s.io/release-utils/log" 27 | ) 28 | 29 | func TestToFile(t *testing.T) { 30 | file, err := os.CreateTemp(t.TempDir(), "log-test-") 31 | require.NoError(t, err) 32 | 33 | defer os.Remove(file.Name()) 34 | 35 | require.NoError(t, log.SetupGlobalLogger("info")) 36 | require.NoError(t, log.ToFile(file.Name())) 37 | logrus.Info("test") 38 | 39 | content, err := os.ReadFile(file.Name()) 40 | require.NoError(t, err) 41 | 42 | require.Contains(t, string(content), "info") 43 | require.Contains(t, string(content), "test") 44 | } 45 | -------------------------------------------------------------------------------- /log/step.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package log 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/sirupsen/logrus" 23 | ) 24 | 25 | // StepLogger is a step counting logger implementation. 26 | type StepLogger struct { 27 | *logrus.Logger 28 | steps uint 29 | currentStep uint 30 | } 31 | 32 | // NewStepLogger creates a new logger. 33 | func NewStepLogger(steps uint) *StepLogger { 34 | return &StepLogger{ 35 | Logger: logrus.StandardLogger(), 36 | steps: steps, 37 | currentStep: 0, 38 | } 39 | } 40 | 41 | // WithStep increments the internal step counter and adds the output to the 42 | // field. 43 | func (l *StepLogger) WithStep() *logrus.Entry { 44 | l.currentStep++ 45 | 46 | return l.WithField( 47 | "step", 48 | fmt.Sprintf("%d/%d", l.currentStep, l.steps), 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /version/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package version provides an importable cobra command and a fixed package 18 | // path location to set compile time version information. To override the 19 | // default values, set the `-ldflags` flags with the following strings: 20 | // 21 | // sigs.k8s.io/release-utils/version.gitVersion= 22 | // sigs.k8s.io/release-utils/version.gitCommit= 23 | // sigs.k8s.io/release-utils/version.gitTreeState= 24 | // sigs.k8s.io/release-utils/version.buildDate= 25 | // 26 | // 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" .` 27 | package version 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: {} 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | 14 | permissions: 15 | contents: write # needed to write releases 16 | 17 | steps: 18 | - name: Set tag name 19 | shell: bash 20 | run: | 21 | echo "TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 22 | 23 | - name: Check out code 24 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 25 | with: 26 | fetch-depth: 1 27 | 28 | - name: Extract version of Go to use 29 | run: echo "GOVERSION=$(awk -F'[:@]' '/FROM golang/{print $2; exit}' Dockerfile.dev)" >> $GITHUB_ENV 30 | 31 | - name: Set up go 32 | uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 33 | with: 34 | go-version: '${{ env.GOVERSION }}' 35 | check-latest: true 36 | cache: false 37 | 38 | - name: Install bom 39 | uses: kubernetes-sigs/release-actions/setup-bom@8af7b2a5596dff526de9db59b2c4b8457e9f52a1 # v0.4.0 40 | 41 | - name: Generate SBOM 42 | shell: bash 43 | run: | 44 | bom generate -c .bom.yaml --format=json -o /tmp/sigs.k8s.io-release-utils-$TAG.spdx.json . 45 | 46 | - name: Publish Release 47 | uses: kubernetes-sigs/release-actions/publish-release@8af7b2a5596dff526de9db59b2c4b8457e9f52a1 # v0.4.0 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | with: 51 | assets: "/tmp/sigs.k8s.io-release-utils-$TAG.spdx.json" 52 | sbom: false 53 | -------------------------------------------------------------------------------- /dependencies.yaml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | # golangci/golangci-lint 3 | - name: "golangci-lint" 4 | version: 2.5.0 5 | refPaths: 6 | - path: mage/golangci-lint.go 7 | 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-]+)*))?" 8 | 9 | # ko 10 | - name: "ko" 11 | version: 0.18.0 12 | refPaths: 13 | - path: mage/ko.go 14 | 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-]+)*))?" 15 | 16 | # cosign 17 | - name: "cosign" 18 | version: 2.6.0 19 | refPaths: 20 | - path: mage/cosign.go 21 | 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-]+)*))?" 22 | 23 | # k8s.io/repo-infra 24 | - name: "repo-infra" 25 | version: 0.2.5 26 | refPaths: 27 | - path: mage/boilerplate.go 28 | 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-]+)*))? 29 | 30 | # sigs.k8s.io/zeitgeist 31 | - name: "zeitgeist" 32 | version: 0.5.4 33 | refPaths: 34 | - path: mage/dependency.go 35 | 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-]+)*))?" 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module sigs.k8s.io/release-utils 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/avast/retry-go/v4 v4.7.0 7 | github.com/blang/semver/v4 v4.0.0 8 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be 9 | github.com/maxbrunsfeld/counterfeiter/v6 v6.12.1 10 | github.com/moby/term v0.5.2 11 | github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 12 | github.com/olekukonko/tablewriter v1.1.2 13 | github.com/sirupsen/logrus v1.9.3 14 | github.com/spf13/cobra v1.10.2 15 | github.com/stretchr/testify v1.11.1 16 | github.com/uwu-tools/magex v0.10.1 17 | k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d 18 | ) 19 | 20 | require ( 21 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect 22 | github.com/Masterminds/semver/v3 v3.3.1 // indirect 23 | github.com/clipperhouse/displaywidth v0.6.0 // indirect 24 | github.com/clipperhouse/stringish v0.1.1 // indirect 25 | github.com/clipperhouse/uax29/v2 v2.3.0 // indirect 26 | github.com/davecgh/go-spew v1.1.1 // indirect 27 | github.com/fatih/color v1.15.0 // indirect 28 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 29 | github.com/kr/pretty v0.3.1 // indirect 30 | github.com/magefile/mage v1.15.0 // indirect 31 | github.com/mattn/go-colorable v0.1.13 // indirect 32 | github.com/mattn/go-isatty v0.0.19 // indirect 33 | github.com/mattn/go-runewidth v0.0.19 // indirect 34 | github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect 35 | github.com/olekukonko/errors v1.1.0 // indirect 36 | github.com/olekukonko/ll v0.1.3 // indirect 37 | github.com/pmezard/go-difflib v1.0.0 // indirect 38 | github.com/spf13/pflag v1.0.9 // indirect 39 | golang.org/x/mod v0.30.0 // indirect 40 | golang.org/x/sync v0.18.0 // indirect 41 | golang.org/x/sys v0.38.0 // indirect 42 | golang.org/x/text v0.31.0 // indirect 43 | golang.org/x/tools v0.39.0 // indirect 44 | gopkg.in/yaml.v3 v3.0.1 // indirect 45 | ) 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | 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: 4 | 5 | _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._ 6 | 7 | ## Getting Started 8 | 9 | We have full documentation on how to get started contributing here: 10 | 11 | 14 | 15 | - [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 16 | - [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) 17 | - [Contributor Cheat Sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet) - Common resources for existing developers 18 | 19 | ## Mentorship 20 | 21 | - [Mentoring Initiatives](https://git.k8s.io/community/mentoring) - We have a diverse set of mentorship programs available that are always looking for volunteers! 22 | 23 | 32 | -------------------------------------------------------------------------------- /mage/cosign.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package mage 18 | 19 | import ( 20 | "fmt" 21 | "log" 22 | "runtime" 23 | 24 | "github.com/uwu-tools/magex/pkg" 25 | "github.com/uwu-tools/magex/pkg/downloads" 26 | ) 27 | 28 | const defaultCosignVersion = "v2.6.0" 29 | 30 | // EnsureCosign makes sure that the specified cosign version is available. 31 | func EnsureCosign(version string) error { 32 | if version == "" { 33 | version = defaultCosignVersion 34 | } 35 | 36 | log.Printf("Checking if `cosign` version %s is installed\n", version) 37 | 38 | found, err := pkg.IsCommandAvailable("cosign", "version", version) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | if !found { 44 | fmt.Println("`cosign` not found") 45 | 46 | return InstallCosign(version) 47 | } 48 | 49 | fmt.Println("`cosign` is installed!") 50 | 51 | return nil 52 | } 53 | 54 | // InstallCosign installs the required cosign version. 55 | func InstallCosign(version string) error { 56 | fmt.Println("Will install `cosign`") 57 | 58 | target := "cosign" 59 | if runtime.GOOS == "windows" { 60 | target = "cosign.exe" 61 | } 62 | 63 | opts := downloads.DownloadOptions{ 64 | UrlTemplate: "https://github.com/sigstore/cosign/releases/download/{{.VERSION}}/cosign-{{.GOOS}}-{{.GOARCH}}", 65 | Name: target, 66 | Version: version, 67 | Ext: "", 68 | } 69 | 70 | return downloads.DownloadToGopathBin(opts) 71 | } 72 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package log 18 | 19 | import ( 20 | "fmt" 21 | "io" 22 | "os" 23 | "strings" 24 | 25 | "github.com/sirupsen/logrus" 26 | 27 | "sigs.k8s.io/release-utils/command" 28 | ) 29 | 30 | // SetupGlobalLogger uses to provided log level string and applies it globally. 31 | func SetupGlobalLogger(level string) error { 32 | logrus.SetFormatter(&logrus.TextFormatter{ 33 | DisableTimestamp: true, 34 | ForceColors: false, 35 | }) 36 | 37 | lvl, err := logrus.ParseLevel(level) 38 | if err != nil { 39 | return fmt.Errorf("setting log level to %s: %w", level, err) 40 | } 41 | 42 | logrus.SetLevel(lvl) 43 | 44 | if lvl >= logrus.DebugLevel { 45 | logrus.Debug("Setting commands globally into verbose mode") 46 | command.SetGlobalVerbose(true) 47 | } 48 | 49 | logrus.AddHook(NewFilenameHook()) 50 | logrus.Debugf("Using log level %q", lvl) 51 | 52 | return nil 53 | } 54 | 55 | // ToFile adds a file destination to the global logger. 56 | func ToFile(fileName string) error { 57 | file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0o755) 58 | if err != nil { 59 | return fmt.Errorf("open log file: %w", err) 60 | } 61 | 62 | writer := io.MultiWriter(logrus.StandardLogger().Out, file) 63 | logrus.SetOutput(writer) 64 | 65 | return nil 66 | } 67 | 68 | // LevelNames returns a comma separated list of available levels. 69 | func LevelNames() string { 70 | levels := []string{} 71 | for _, level := range logrus.AllLevels { 72 | levels = append(levels, fmt.Sprintf("'%s'", level.String())) 73 | } 74 | 75 | return strings.Join(levels, ", ") 76 | } 77 | -------------------------------------------------------------------------------- /version/command.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package version 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | // Version returns a cobra command to be added to another cobra command, like: 26 | // ```go 27 | // 28 | // rootCmd.AddCommand(version.Version()) 29 | // 30 | // ```. 31 | func Version() *cobra.Command { 32 | return version("") 33 | } 34 | 35 | // WithFont returns a cobra command to be added to another cobra command with a select font for ASCII, like: 36 | // ```go 37 | // 38 | // rootCmd.AddCommand(version.WithFont("starwars")) 39 | // 40 | // ```. 41 | func WithFont(fontName string) *cobra.Command { 42 | return version(fontName) 43 | } 44 | 45 | func version(fontName string) *cobra.Command { 46 | var outputJSON bool 47 | 48 | cmd := &cobra.Command{ 49 | Use: "version", 50 | Short: "Prints the version", 51 | RunE: func(cmd *cobra.Command, _ []string) error { 52 | v := GetVersionInfo() 53 | v.Name = cmd.Root().Name() 54 | v.Description = cmd.Root().Short 55 | 56 | v.FontName = "" 57 | if fontName != "" && v.CheckFontName(fontName) { 58 | v.FontName = fontName 59 | } 60 | 61 | cmd.SetOut(cmd.OutOrStdout()) 62 | 63 | if outputJSON { 64 | out, err := v.JSONString() 65 | if err != nil { 66 | return fmt.Errorf("unable to generate JSON from version info: %w", err) 67 | } 68 | 69 | cmd.Println(out) 70 | } else { 71 | cmd.Println(v.String()) 72 | } 73 | 74 | return nil 75 | }, 76 | } 77 | 78 | cmd.Flags().BoolVar(&outputJSON, "json", false, "print JSON instead of text") 79 | 80 | return cmd 81 | } 82 | -------------------------------------------------------------------------------- /http/example_multi_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package http_test 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "io" 23 | "os" 24 | 25 | "github.com/sirupsen/logrus" 26 | 27 | "sigs.k8s.io/release-utils/http" 28 | ) 29 | 30 | func Example() { 31 | // This example fetches 10 photographs from flick in parallel 32 | agent := http.NewAgent() 33 | w := []io.Writer{} 34 | urls := []string{ 35 | "https://live.staticflickr.com/65535/53863838503_3490725fab.jpg", 36 | "https://live.staticflickr.com/65535/53862224352_a9949bb818.jpg", 37 | "https://live.staticflickr.com/65535/53863076331_570818d62f_w.jpg", 38 | "https://live.staticflickr.com/65535/53863751331_aa8cc7c233_w.jpg", 39 | "https://live.staticflickr.com/65535/53862636262_3ec860a652.jpg", 40 | "https://live.staticflickr.com/65535/53863034561_079ea0a87b_z.jpg", 41 | "https://live.staticflickr.com/65535/53862940596_5a991b2271_w.jpg", 42 | "https://live.staticflickr.com/65535/53863423169_90f8e13b7f_z", 43 | "https://live.staticflickr.com/65535/53863136849_965bd39df1_n.jpg", 44 | "https://live.staticflickr.com/65535/53863672556_1050bbf01b_n.jpg", 45 | } 46 | 47 | for i := range urls { 48 | f, err := os.Create(fmt.Sprintf("/tmp/photo-%d.jpg", i)) 49 | if err != nil { 50 | logrus.Fatal("error opening file") 51 | } 52 | 53 | w = append(w, f) 54 | } 55 | 56 | defer func() { 57 | for i := range w { 58 | w[i].(*os.File).Close() 59 | } 60 | }() 61 | 62 | errs := agent.GetToWriterGroup(w, urls) 63 | if errors.Join(errs...) != nil { 64 | logrus.Fatalf("%d errors fetching photos: %v", len(errs), errors.Join(errs...)) 65 | } 66 | // output: 67 | } 68 | -------------------------------------------------------------------------------- /hash/hash.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package hash 18 | 19 | import ( 20 | "crypto/sha1" //nolint: gosec 21 | "crypto/sha256" 22 | "crypto/sha512" 23 | "encoding/hex" 24 | "errors" 25 | "fmt" 26 | "hash" 27 | "io" 28 | "os" 29 | 30 | "github.com/sirupsen/logrus" 31 | ) 32 | 33 | // SHA512ForFile returns the hex-encoded sha512 hash for the provided filename. 34 | func SHA512ForFile(filename string) (string, error) { 35 | return ForFile(filename, sha512.New()) 36 | } 37 | 38 | // SHA256ForFile returns the hex-encoded sha256 hash for the provided filename. 39 | func SHA256ForFile(filename string) (string, error) { 40 | return ForFile(filename, sha256.New()) 41 | } 42 | 43 | // SHA1ForFile returns the hex-encoded sha1 hash for the provided filename. 44 | // TODO: check if we can remove this function. 45 | func SHA1ForFile(filename string) (string, error) { 46 | return ForFile(filename, sha1.New()) //nolint: gosec 47 | } 48 | 49 | // ForFile returns the hex-encoded hash for the provided filename and hasher. 50 | func ForFile(filename string, hasher hash.Hash) (string, error) { 51 | if hasher == nil { 52 | return "", errors.New("provided hasher is nil") 53 | } 54 | 55 | f, err := os.Open(filename) 56 | if err != nil { 57 | return "", fmt.Errorf("open file %s: %w", filename, err) 58 | } 59 | 60 | defer func() { 61 | if err := f.Close(); err != nil { 62 | logrus.Warnf("Unable to close file %q: %v", filename, err) 63 | } 64 | }() 65 | 66 | hasher.Reset() 67 | 68 | if _, err := io.Copy(hasher, f); err != nil { 69 | return "", fmt.Errorf("hash file %s: %w", filename, err) 70 | } 71 | 72 | return hex.EncodeToString(hasher.Sum(nil)), nil 73 | } 74 | -------------------------------------------------------------------------------- /magefile.go: -------------------------------------------------------------------------------- 1 | //go:build mage 2 | // +build mage 3 | 4 | /* 5 | Copyright 2021 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "fmt" 24 | "path/filepath" 25 | 26 | "sigs.k8s.io/release-utils/mage" 27 | ) 28 | 29 | // Default target to run when none is specified 30 | // If not set, running mage will list available targets 31 | var Default = Verify 32 | 33 | const ( 34 | binDir = "bin" 35 | scriptDir = "scripts" 36 | ) 37 | 38 | var boilerplateDir = filepath.Join(scriptDir, "boilerplate") 39 | 40 | // All runs all targets for this repository 41 | func All() error { 42 | if err := Verify(); err != nil { 43 | return err 44 | } 45 | 46 | if err := Test(); err != nil { 47 | return err 48 | } 49 | 50 | return nil 51 | } 52 | 53 | // Test runs various test functions 54 | func Test() error { 55 | if err := mage.TestGo(true); err != nil { 56 | return err 57 | } 58 | 59 | return nil 60 | } 61 | 62 | // Verify runs repository verification scripts 63 | func Verify() error { 64 | fmt.Println("Running copyright header checks...") 65 | if err := mage.VerifyBoilerplate("", binDir, boilerplateDir, true); err != nil { 66 | return err 67 | } 68 | 69 | fmt.Println("Running external dependency checks...") 70 | if err := mage.VerifyDeps("", "", "", true); err != nil { 71 | return err 72 | } 73 | 74 | fmt.Println("Running go module linter...") 75 | if err := mage.VerifyGoMod(); err != nil { 76 | return err 77 | } 78 | 79 | fmt.Println("Running golangci-lint...") 80 | if err := mage.RunGolangCILint("", false); err != nil { 81 | return err 82 | } 83 | 84 | fmt.Println("Running go build...") 85 | if err := mage.VerifyBuild(scriptDir); err != nil { 86 | return err 87 | } 88 | 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /mage/ko.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package mage 18 | 19 | import ( 20 | "fmt" 21 | "runtime" 22 | 23 | "github.com/uwu-tools/magex/pkg" 24 | "github.com/uwu-tools/magex/pkg/archive" 25 | "github.com/uwu-tools/magex/pkg/downloads" 26 | ) 27 | 28 | const defaultKoVersion = "0.18.0" 29 | 30 | // EnsureKO ensures that the ko binary exists. 31 | func EnsureKO(version string) error { 32 | if version == "" { 33 | version = defaultKoVersion 34 | } 35 | 36 | fmt.Printf("Checking if `ko` version %s is installed\n", version) 37 | 38 | found, err := pkg.IsCommandAvailable("ko", "version", version) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | if !found { 44 | fmt.Println("`ko` not found") 45 | 46 | return InstallKO(version) 47 | } 48 | 49 | fmt.Println("`ko` is installed!") 50 | 51 | return nil 52 | } 53 | 54 | // Maybe we can move this to release-utils. 55 | func InstallKO(version string) error { 56 | fmt.Println("Will install `ko`") 57 | 58 | target := "ko" 59 | if runtime.GOOS == "windows" { 60 | target = "ko.exe" 61 | } 62 | 63 | opts := archive.DownloadArchiveOptions{ 64 | DownloadOptions: downloads.DownloadOptions{ 65 | UrlTemplate: "https://github.com/ko-build/ko/releases/download/v{{.VERSION}}/ko_{{.VERSION}}_{{.GOOS}}_{{.GOARCH}}{{.EXT}}", 66 | Name: "ko", 67 | Version: version, 68 | OsReplacement: map[string]string{ 69 | "darwin": "Darwin", 70 | "linux": "Linux", 71 | "windows": "Windows", 72 | }, 73 | ArchReplacement: map[string]string{ 74 | "amd64": "x86_64", 75 | }, 76 | }, 77 | ArchiveExtensions: map[string]string{ 78 | "linux": ".tar.gz", 79 | "darwin": ".tar.gz", 80 | "windows": ".tar.gz", 81 | }, 82 | TargetFileTemplate: target, 83 | } 84 | 85 | return archive.DownloadToGopathBin(opts) 86 | } 87 | -------------------------------------------------------------------------------- /editor/editor_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package editor 18 | 19 | import ( 20 | "bytes" 21 | "os" 22 | "reflect" 23 | "strings" 24 | "testing" 25 | ) 26 | 27 | func TestArgs(t *testing.T) { 28 | if e, a := []string{"/bin/bash", "-c \"test\""}, (Editor{Args: []string{"/bin/bash", "-c"}, Shell: true}).args("test"); !reflect.DeepEqual(e, a) { 29 | t.Errorf("unexpected args: %v", a) 30 | } 31 | 32 | if e, a := []string{"/bin/bash", "-c", "test"}, (Editor{Args: []string{"/bin/bash", "-c"}, Shell: false}).args("test"); !reflect.DeepEqual(e, a) { 33 | t.Errorf("unexpected args: %v", a) 34 | } 35 | 36 | if e, a := []string{"/bin/bash", "-i -c \"test\""}, (Editor{Args: []string{"/bin/bash", "-i -c"}, Shell: true}).args("test"); !reflect.DeepEqual(e, a) { 37 | t.Errorf("unexpected args: %v", a) 38 | } 39 | 40 | if e, a := []string{"/test", "test"}, (Editor{Args: []string{"/test"}}).args("test"); !reflect.DeepEqual(e, a) { 41 | t.Errorf("unexpected args: %v", a) 42 | } 43 | } 44 | 45 | func TestEditor(t *testing.T) { 46 | edit := Editor{Args: []string{"cat"}} 47 | testStr := "test something\n" 48 | 49 | contents, path, err := edit.LaunchTempFile("", "someprefix", bytes.NewBufferString(testStr)) 50 | if err != nil { 51 | t.Fatalf("unexpected error: %v", err) 52 | } 53 | 54 | if _, err := os.Stat(path); err != nil { 55 | t.Fatalf("no temp file: %s", path) 56 | } 57 | 58 | defer os.Remove(path) 59 | 60 | if disk, err := os.ReadFile(path); err != nil || !bytes.Equal(contents, disk) { 61 | t.Errorf("unexpected file on disk: %v %s", err, string(disk)) 62 | } 63 | 64 | if !bytes.Equal(contents, []byte(testStr)) { 65 | t.Errorf("unexpected contents: %s", string(contents)) 66 | } 67 | 68 | if !strings.Contains(path, "someprefix") { 69 | t.Errorf("path not expected: %s", path) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /mage/git.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package mage 18 | 19 | import ( 20 | "fmt" 21 | 22 | "sigs.k8s.io/release-utils/command" 23 | ) 24 | 25 | const ( 26 | gitConfigNameKey = "user.name" 27 | gitConfigNameValue = "releng-ci-user" 28 | gitConfigEmailKey = "user.email" 29 | gitConfigEmailValue = "nobody@k8s.io" 30 | ) 31 | 32 | func CheckGitConfigExists() bool { 33 | userName := command.New( 34 | "git", 35 | "config", 36 | "--global", 37 | "--get", 38 | gitConfigNameKey, 39 | ) 40 | 41 | stream, err := userName.RunSilentSuccessOutput() 42 | if err != nil || stream.OutputTrimNL() == "" { 43 | // NB: We're intentionally ignoring the error here because 'git config' 44 | // returns an error (result code -1) if the config doesn't exist. 45 | return false 46 | } 47 | 48 | userEmail := command.New( 49 | "git", 50 | "config", 51 | "--global", 52 | "--get", 53 | gitConfigEmailKey, 54 | ) 55 | 56 | stream, err = userEmail.RunSilentSuccessOutput() 57 | if err != nil || stream.OutputTrimNL() == "" { 58 | // NB: We're intentionally ignoring the error here because 'git config' 59 | // returns an error (result code -1) if the config doesn't exist. 60 | return false 61 | } 62 | 63 | return true 64 | } 65 | 66 | func EnsureGitConfig() error { 67 | exists := CheckGitConfigExists() 68 | if exists { 69 | return nil 70 | } 71 | 72 | if err := command.New( 73 | "git", 74 | "config", 75 | "--global", 76 | gitConfigNameKey, 77 | gitConfigNameValue, 78 | ).RunSuccess(); err != nil { 79 | return fmt.Errorf("configuring git %s: %w", gitConfigNameKey, err) 80 | } 81 | 82 | if err := command.New( 83 | "git", 84 | "config", 85 | "--global", 86 | gitConfigEmailKey, 87 | gitConfigEmailValue, 88 | ).RunSuccess(); err != nil { 89 | return fmt.Errorf("configuring git %s: %w", gitConfigEmailKey, err) 90 | } 91 | 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /editor/tty.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package editor 18 | 19 | import ( 20 | "io" 21 | "os" 22 | 23 | "github.com/moby/term" 24 | "github.com/sirupsen/logrus" 25 | ) 26 | 27 | // TTY helps invoke a function and preserve the state of the terminal, even if the process is 28 | // terminated during execution. It also provides support for terminal resizing for remote command 29 | // execution/attachment. 30 | type TTY struct { 31 | // In is a reader representing stdin. It is a required field. 32 | In io.Reader 33 | // Out is a writer representing stdout. It must be set to support terminal resizing. It is an 34 | // optional field. 35 | Out io.Writer 36 | // Raw is true if the terminal should be set raw. 37 | Raw bool 38 | // TryDev indicates the TTY should try to open /dev/tty if the provided input 39 | // is not a file descriptor. 40 | TryDev bool 41 | } 42 | 43 | // Safe invokes the provided function and will attempt to ensure that when the 44 | // function returns (or a termination signal is sent) that the terminal state 45 | // is reset to the condition it was in prior to the function being invoked. If 46 | // t.Raw is true the terminal will be put into raw mode prior to calling the function. 47 | // If the input file descriptor is not a TTY and TryDev is true, the /dev/tty file 48 | // will be opened (if available). 49 | func (t TTY) Safe(fn func() error) error { 50 | inFd, isTerminal := term.GetFdInfo(t.In) 51 | 52 | if !isTerminal && t.TryDev { 53 | if f, err := os.Open("/dev/tty"); err == nil { 54 | defer f.Close() 55 | 56 | inFd = f.Fd() 57 | isTerminal = term.IsTerminal(inFd) 58 | } 59 | } 60 | 61 | if !isTerminal { 62 | return fn() 63 | } 64 | 65 | var state *term.State 66 | 67 | var err error 68 | if t.Raw { 69 | state, err = term.MakeRaw(inFd) 70 | } else { 71 | state, err = term.SaveState(inFd) 72 | } 73 | 74 | if err != nil { 75 | return err 76 | } 77 | 78 | defer func() { 79 | if err := term.RestoreTerminal(inFd, state); err != nil { 80 | logrus.Errorf("Error resetting terminal: %v", err) 81 | } 82 | }() 83 | 84 | return fn() 85 | } 86 | -------------------------------------------------------------------------------- /helpers/tablewriter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package helpers 18 | 19 | import ( 20 | "io" 21 | 22 | "github.com/olekukonko/tablewriter" 23 | "github.com/olekukonko/tablewriter/renderer" 24 | "github.com/olekukonko/tablewriter/tw" 25 | ) 26 | 27 | // NewTableWriter creates a new table writer with the given output and options. 28 | func NewTableWriter(output io.Writer, options ...tablewriter.Option) *tablewriter.Table { 29 | table := tablewriter.NewWriter(output) 30 | for _, opt := range options { 31 | table.Options(opt) 32 | } 33 | 34 | return table 35 | } 36 | 37 | // NewTableWriterWithDefaults creates a new table writer with default markdown configuration. 38 | // It includes left alignment, markdown renderer, and custom borders optimized for terminal output. 39 | func NewTableWriterWithDefaults(output io.Writer, options ...tablewriter.Option) *tablewriter.Table { 40 | defaultOptions := []tablewriter.Option{ 41 | tablewriter.WithConfig(tablewriter.Config{ 42 | Header: tw.CellConfig{ 43 | Alignment: tw.CellAlignment{Global: tw.AlignLeft}, 44 | }, 45 | }), 46 | tablewriter.WithRenderer(renderer.NewMarkdown()), 47 | tablewriter.WithRendition(tw.Rendition{ 48 | Symbols: tw.NewSymbols(tw.StyleMarkdown), 49 | Borders: tw.Border{ 50 | Left: tw.On, 51 | Top: tw.Off, 52 | Right: tw.On, 53 | Bottom: tw.Off, 54 | }, 55 | Settings: tw.Settings{ 56 | Separators: tw.Separators{ 57 | BetweenRows: tw.On, 58 | }, 59 | }, 60 | }), 61 | tablewriter.WithRowAutoWrap(tw.WrapNone), 62 | } 63 | 64 | defaultOptions = append(defaultOptions, options...) 65 | 66 | return NewTableWriter(output, defaultOptions...) 67 | } 68 | 69 | // NewTableWriterWithDefaultsAndHeader creates a new table writer with default configuration and header. 70 | func NewTableWriterWithDefaultsAndHeader(output io.Writer, header []string, options ...tablewriter.Option) *tablewriter.Table { 71 | headerOption := tablewriter.WithHeader(header) 72 | allOptions := append([]tablewriter.Option{headerOption}, options...) 73 | 74 | return NewTableWriterWithDefaults(output, allOptions...) 75 | } 76 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 26 | 27 | #### What type of PR is this? 28 | 29 | 33 | 34 | 47 | 48 | #### What this PR does / why we need it: 49 | 50 | #### Which issue(s) this PR fixes: 51 | 52 | 56 | 57 | 62 | 63 | #### Special notes for your reviewer: 64 | 65 | #### Does this PR introduce a user-facing change? 66 | 67 | 76 | 77 | ```release-note 78 | 79 | ``` 80 | -------------------------------------------------------------------------------- /mage/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package mage 18 | 19 | import ( 20 | "fmt" 21 | "strconv" 22 | "time" 23 | 24 | "github.com/uwu-tools/magex/shx" 25 | ) 26 | 27 | // getVersion gets a description of the commit, e.g. v0.30.1 (latest) or v0.30.1-32-gfe72ff73 (canary). 28 | func getVersion() (string, error) { 29 | version, err := shx.Output("git", "describe", "--tags", "--always") 30 | if err != nil { 31 | return "", err 32 | } 33 | 34 | if version != "" { 35 | return version, nil 36 | } 37 | 38 | // repo without any tags in it 39 | return "v0.0.0", nil 40 | } 41 | 42 | // getCommit gets the hash of the current commit. 43 | func getCommit() (string, error) { 44 | return shx.Output("git", "rev-parse", "--short", "HEAD") 45 | } 46 | 47 | // getGitState gets the state of the git repository. 48 | func getGitState() string { 49 | _, err := shx.Output("git", "diff", "--quiet") 50 | if err != nil { 51 | return "dirty" 52 | } 53 | 54 | return "clean" 55 | } 56 | 57 | // getBuildDateTime gets the build date and time. 58 | func getBuildDateTime() (string, error) { 59 | result, err := shx.Output("git", "log", "-1", "--pretty=%ct") 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | if result != "" { 65 | parsedInt, err := strconv.ParseInt(result, 10, 64) 66 | if err != nil { 67 | return "", fmt.Errorf("parse source date epoch to int: %w", err) 68 | } 69 | 70 | return time.Unix(parsedInt, 0).UTC().Format(time.RFC3339), nil 71 | } 72 | 73 | return shx.Output("date", "+%Y-%m-%dT%H:%M:%SZ") 74 | } 75 | 76 | // GenerateLDFlags returns the string to use in the `-ldflags` flag. 77 | func GenerateLDFlags() (string, error) { 78 | pkg := "sigs.k8s.io/release-utils/version" 79 | 80 | version, err := getVersion() 81 | if err != nil { 82 | return "", err 83 | } 84 | 85 | commit, err := getCommit() 86 | if err != nil { 87 | return "", err 88 | } 89 | 90 | buildTime, err := getBuildDateTime() 91 | if err != nil { 92 | return "", err 93 | } 94 | 95 | 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", 96 | pkg, version, commit, getGitState(), buildTime), nil 97 | } 98 | -------------------------------------------------------------------------------- /env/env_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package env 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/require" 23 | 24 | "sigs.k8s.io/release-utils/env/internal" 25 | "sigs.k8s.io/release-utils/env/internal/internalfakes" 26 | ) 27 | 28 | func TestDefault(t *testing.T) { 29 | for _, tc := range []struct { 30 | prepare func(*internalfakes.FakeImpl) 31 | defaultValue string 32 | expected string 33 | }{ 34 | { // default LookupEnvReturns empty string and false 35 | prepare: func(mock *internalfakes.FakeImpl) { 36 | mock.LookupEnvReturns("", false) 37 | }, 38 | defaultValue: "default", 39 | expected: "default", 40 | }, 41 | { // default LookupEnvReturns empty string and true 42 | prepare: func(mock *internalfakes.FakeImpl) { 43 | mock.LookupEnvReturns("", true) 44 | }, 45 | defaultValue: "default", 46 | expected: "default", 47 | }, 48 | { // default LookupEnvReturns string and false 49 | prepare: func(mock *internalfakes.FakeImpl) { 50 | mock.LookupEnvReturns("value", false) 51 | }, 52 | defaultValue: "default", 53 | expected: "default", 54 | }, 55 | { // value is set 56 | prepare: func(mock *internalfakes.FakeImpl) { 57 | mock.LookupEnvReturns("value", true) 58 | }, 59 | defaultValue: "default", 60 | expected: "value", 61 | }, 62 | } { 63 | mock := &internalfakes.FakeImpl{} 64 | tc.prepare(mock) 65 | internal.Impl = mock 66 | 67 | res := Default("key", tc.defaultValue) 68 | require.Equal(t, tc.expected, res) 69 | } 70 | } 71 | 72 | func TestIsSet(t *testing.T) { 73 | for _, tc := range []struct { 74 | prepare func(*internalfakes.FakeImpl) 75 | expected bool 76 | }{ 77 | { // LookupEnvReturns false 78 | prepare: func(mock *internalfakes.FakeImpl) { 79 | mock.LookupEnvReturns("", false) 80 | }, 81 | expected: false, 82 | }, 83 | { // LookupEnvReturns true 84 | prepare: func(mock *internalfakes.FakeImpl) { 85 | mock.LookupEnvReturns("", true) 86 | }, 87 | expected: true, 88 | }, 89 | } { 90 | mock := &internalfakes.FakeImpl{} 91 | tc.prepare(mock) 92 | internal.Impl = mock 93 | 94 | res := IsSet("key") 95 | require.Equal(t, tc.expected, res) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # NFS 2 | .nfs* 3 | 4 | # OSX leaves these everywhere on SMB shares 5 | ._* 6 | 7 | # OSX trash 8 | .DS_Store 9 | 10 | # Eclipse files 11 | .classpath 12 | .project 13 | .settings/** 14 | 15 | # Files generated by JetBrains IDEs, e.g. IntelliJ IDEA 16 | .idea/ 17 | *.iml 18 | 19 | # Vscode files 20 | .vscode 21 | 22 | # This is where the result of the go build goes 23 | /output*/ 24 | /_output*/ 25 | /_output 26 | 27 | # Emacs save files 28 | *~ 29 | \#*\# 30 | .\#* 31 | 32 | # Vim-related files 33 | [._]*.s[a-w][a-z] 34 | [._]s[a-w][a-z] 35 | *.un~ 36 | Session.vim 37 | .netrwhist 38 | 39 | # Go test binaries 40 | *.test 41 | /hack/.test-cmd-auth 42 | 43 | # JUnit test output from ginkgo e2e tests 44 | /junit*.xml 45 | 46 | # Mercurial files 47 | **/.hg 48 | **/.hg* 49 | 50 | # Vagrant 51 | .vagrant 52 | network_closure.sh 53 | 54 | # Local cluster env variables 55 | /cluster/env.sh 56 | 57 | # Compiled binaries in third_party 58 | /third_party/pkg 59 | 60 | # Also ignore etcd installed by hack/install-etcd.sh 61 | /third_party/etcd* 62 | 63 | # User cluster configs 64 | .kubeconfig 65 | 66 | .tags* 67 | 68 | # Version file for dockerized build 69 | .dockerized-kube-version-defs 70 | 71 | # Web UI 72 | /www/master/node_modules/ 73 | /www/master/npm-debug.log 74 | /www/master/shared/config/development.json 75 | 76 | # Karma output 77 | /www/test_out 78 | 79 | # precommit temporary directories created by ./hack/verify-generated-docs.sh and ./hack/lib/util.sh 80 | /_tmp/ 81 | /doc_tmp/ 82 | 83 | # Test artifacts produced by Jenkins jobs 84 | /_artifacts/ 85 | 86 | # Go dependencies installed on Jenkins 87 | /_gopath/ 88 | 89 | # Config directories created by gcloud and gsutil on Jenkins 90 | /.config/gcloud*/ 91 | /.gsutil/ 92 | 93 | # CoreOS stuff 94 | /cluster/libvirt-coreos/coreos_*.img 95 | 96 | # Juju Stuff 97 | /cluster/juju/charms/* 98 | /cluster/juju/bundles/local.yaml 99 | 100 | # Downloaded Kubernetes binary release 101 | /kubernetes/ 102 | 103 | # direnv .envrc files 104 | .envrc 105 | 106 | # Downloaded kubernetes binary release tar ball 107 | kubernetes.tar.gz 108 | 109 | # generated files in any directory 110 | # TODO(thockin): uncomment this when we stop committing the generated files. 111 | #zz_generated.* 112 | 113 | # make-related metadata 114 | /.make/ 115 | # Just in time generated data in the source, should never be committed 116 | /test/e2e/generated/bindata.go 117 | 118 | # This file used by some vendor repos (e.g. github.com/go-openapi/...) to store secret variables and should not be ignored 119 | !\.drone\.sec 120 | 121 | /bazel-* 122 | 123 | # vendored go modules 124 | /vendor 125 | 126 | # git merge conflict originals 127 | *.orig 128 | 129 | # go coverage files 130 | coverage.* 131 | 132 | # test files 133 | tmp 134 | CHANGELOG-*.html 135 | 136 | # downloaded and built binaries 137 | bin 138 | qemu-*-static 139 | rootfs.tar 140 | -------------------------------------------------------------------------------- /http/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | Package http provides a configurable agent to talk to http servers. 19 | 20 | # Function Families 21 | 22 | It provides three families of functions for the GET, POST and HEAD methods 23 | that return the raw http.Response, the response contents as a byte slice or to 24 | write the response to a writer. 25 | 26 | Each of these functions also provide a _Group_ equivalent that takes a list 27 | of URLs and performs the requests in parallel. The easiest way to understand 28 | the functions is this expression: 29 | 30 | METHOD[Request|ToWriter][Group] 31 | 32 | So, for examaple, the functions for the POST method include the following 33 | variations, note that the Group variants take and return the same arguments 34 | but in plural form, ie same type but a slice: 35 | 36 | Post(string url, []byte postData) ([]byte, error) 37 | PostRequest(string url, []byte postData) (*http.Response, error) 38 | PostToWriter(io.Writer w, string url, []byte postData) error 39 | PostGroup([]string urls, [][]byte postData) ([][]byte, []error) 40 | PostRequestGroup([]string urls, [][]byte postData) ([]*http.Response, []error) 41 | PostToWriterGroup([]io.Writer w, []string urls, [][]byte postData) []error 42 | 43 | # Group Requests 44 | 45 | All the _Group_ families perform the requests in parallel. The number of 46 | simultaneous requests can be controlled with the .WithMaxParallel(int) option: 47 | 48 | # Create an HTTP agent that performs two requests at a time: 49 | agent := http.NewAgent().WithMaxParallel(2) 50 | 51 | All group requests take arguments in slices and return data and errors in slices 52 | guaranteed to be of the same length and order as the arguments. 53 | 54 | To check the returned error slice for success in a single shot the errors.Join() 55 | function comes in handy: 56 | 57 | responses, errs := agent.GetGroup(urlList) 58 | if errors.Join(errs) != nil { 59 | // Handle errors here 60 | } 61 | 62 | # Single and Multiple Writer Output 63 | 64 | The ToWriterGroup variants take a list of writers in their first arguments. 65 | Usually, the data returned by the requests will be written to each corresponding 66 | writer in the slice (eg request #5 to writer #5). There is an exception though, 67 | if the writer slice contains a single writer, the data from all requests will 68 | be written - in order - into the single writer. This allows for simple piping to 69 | a single output sink (ie all output to STDOUT). 70 | 71 | # Example 72 | 73 | The following example shows a code snippet that fetches ten photographs in parallel 74 | and writes them to disk. 75 | */ 76 | package http 77 | -------------------------------------------------------------------------------- /log/hooks.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package log 18 | 19 | import ( 20 | "fmt" 21 | "runtime" 22 | "strings" 23 | 24 | "github.com/sirupsen/logrus" 25 | ) 26 | 27 | type FileNameHook struct { 28 | field string 29 | skipPrefix []string 30 | formatter logrus.Formatter 31 | Formatter func(file, function string, line int) string 32 | } 33 | 34 | type wrapper struct { 35 | old logrus.Formatter 36 | hook *FileNameHook 37 | } 38 | 39 | // NewFilenameHook creates a new default FileNameHook. 40 | func NewFilenameHook() *FileNameHook { 41 | return &FileNameHook{ 42 | field: "file", 43 | skipPrefix: []string{"log/", "logrus/", "logrus@"}, 44 | Formatter: func(file, _ string, line int) string { 45 | return fmt.Sprintf("%s:%d", file, line) 46 | }, 47 | } 48 | } 49 | 50 | // Levels returns the levels for which the hook is activated. This contains 51 | // currently only the DebugLevel. 52 | func (f *FileNameHook) Levels() []logrus.Level { 53 | return []logrus.Level{logrus.DebugLevel} 54 | } 55 | 56 | // Fire executes the hook for every logrus entry. 57 | func (f *FileNameHook) Fire(entry *logrus.Entry) error { 58 | if f.formatter != entry.Logger.Formatter { 59 | f.formatter = &wrapper{entry.Logger.Formatter, f} 60 | } 61 | 62 | entry.Logger.Formatter = f.formatter 63 | 64 | return nil 65 | } 66 | 67 | // Format returns the log format including the caller as field. 68 | func (w *wrapper) Format(entry *logrus.Entry) ([]byte, error) { 69 | field := entry.WithField( 70 | w.hook.field, 71 | w.hook.Formatter(w.hook.findCaller()), 72 | ) 73 | field.Level = entry.Level 74 | field.Message = entry.Message 75 | 76 | return w.old.Format(field) 77 | } 78 | 79 | // findCaller returns the file, function and line number for the current call. 80 | func (f *FileNameHook) findCaller() (file, function string, line int) { 81 | var pc uintptr 82 | // The maximum amount of frames to be iterated 83 | const maxFrames = 10 84 | for i := range maxFrames { 85 | // The amount of frames to be skipped to land at the actual caller 86 | const skipFrames = 5 87 | 88 | pc, file, line = caller(skipFrames + i) 89 | if !f.shouldSkipPrefix(file) { 90 | break 91 | } 92 | } 93 | 94 | if pc != 0 { 95 | frames := runtime.CallersFrames([]uintptr{pc}) 96 | frame, _ := frames.Next() 97 | function = frame.Function 98 | } 99 | 100 | return file, function, line 101 | } 102 | 103 | // caller reports file and line number information about function invocations 104 | // on the calling goroutine's stack. The argument skip is the number of stack 105 | // frames to ascend, with 0 identifying the caller of Caller. 106 | func caller(skip int) (pc uintptr, file string, line int) { 107 | ok := false 108 | pc, file, line, ok = runtime.Caller(skip) 109 | 110 | if !ok { 111 | return 0, "", 0 112 | } 113 | 114 | n := 0 115 | 116 | for i := len(file) - 1; i > 0; i-- { 117 | if file[i] == '/' { 118 | n++ 119 | if n >= 2 { 120 | file = file[i+1:] 121 | 122 | break 123 | } 124 | } 125 | } 126 | 127 | return pc, file, line 128 | } 129 | 130 | // shouldSkipPrefix returns true if the hook should be skipped, otherwise false. 131 | func (f *FileNameHook) shouldSkipPrefix(file string) bool { 132 | for i := range f.skipPrefix { 133 | if strings.HasPrefix(file, f.skipPrefix[i]) { 134 | return true 135 | } 136 | } 137 | 138 | return false 139 | } 140 | -------------------------------------------------------------------------------- /env/internal/internalfakes/fake_impl.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by counterfeiter. DO NOT EDIT. 18 | package internalfakes 19 | 20 | import ( 21 | "sync" 22 | ) 23 | 24 | type FakeImpl struct { 25 | LookupEnvStub func(string) (string, bool) 26 | lookupEnvMutex sync.RWMutex 27 | lookupEnvArgsForCall []struct { 28 | arg1 string 29 | } 30 | lookupEnvReturns struct { 31 | result1 string 32 | result2 bool 33 | } 34 | lookupEnvReturnsOnCall map[int]struct { 35 | result1 string 36 | result2 bool 37 | } 38 | invocations map[string][][]interface{} 39 | invocationsMutex sync.RWMutex 40 | } 41 | 42 | func (fake *FakeImpl) LookupEnv(arg1 string) (string, bool) { 43 | fake.lookupEnvMutex.Lock() 44 | ret, specificReturn := fake.lookupEnvReturnsOnCall[len(fake.lookupEnvArgsForCall)] 45 | fake.lookupEnvArgsForCall = append(fake.lookupEnvArgsForCall, struct { 46 | arg1 string 47 | }{arg1}) 48 | stub := fake.LookupEnvStub 49 | fakeReturns := fake.lookupEnvReturns 50 | fake.recordInvocation("LookupEnv", []interface{}{arg1}) 51 | fake.lookupEnvMutex.Unlock() 52 | if stub != nil { 53 | return stub(arg1) 54 | } 55 | if specificReturn { 56 | return ret.result1, ret.result2 57 | } 58 | return fakeReturns.result1, fakeReturns.result2 59 | } 60 | 61 | func (fake *FakeImpl) LookupEnvCallCount() int { 62 | fake.lookupEnvMutex.RLock() 63 | defer fake.lookupEnvMutex.RUnlock() 64 | return len(fake.lookupEnvArgsForCall) 65 | } 66 | 67 | func (fake *FakeImpl) LookupEnvCalls(stub func(string) (string, bool)) { 68 | fake.lookupEnvMutex.Lock() 69 | defer fake.lookupEnvMutex.Unlock() 70 | fake.LookupEnvStub = stub 71 | } 72 | 73 | func (fake *FakeImpl) LookupEnvArgsForCall(i int) string { 74 | fake.lookupEnvMutex.RLock() 75 | defer fake.lookupEnvMutex.RUnlock() 76 | argsForCall := fake.lookupEnvArgsForCall[i] 77 | return argsForCall.arg1 78 | } 79 | 80 | func (fake *FakeImpl) LookupEnvReturns(result1 string, result2 bool) { 81 | fake.lookupEnvMutex.Lock() 82 | defer fake.lookupEnvMutex.Unlock() 83 | fake.LookupEnvStub = nil 84 | fake.lookupEnvReturns = struct { 85 | result1 string 86 | result2 bool 87 | }{result1, result2} 88 | } 89 | 90 | func (fake *FakeImpl) LookupEnvReturnsOnCall(i int, result1 string, result2 bool) { 91 | fake.lookupEnvMutex.Lock() 92 | defer fake.lookupEnvMutex.Unlock() 93 | fake.LookupEnvStub = nil 94 | if fake.lookupEnvReturnsOnCall == nil { 95 | fake.lookupEnvReturnsOnCall = make(map[int]struct { 96 | result1 string 97 | result2 bool 98 | }) 99 | } 100 | fake.lookupEnvReturnsOnCall[i] = struct { 101 | result1 string 102 | result2 bool 103 | }{result1, result2} 104 | } 105 | 106 | func (fake *FakeImpl) Invocations() map[string][][]interface{} { 107 | fake.invocationsMutex.RLock() 108 | defer fake.invocationsMutex.RUnlock() 109 | fake.lookupEnvMutex.RLock() 110 | defer fake.lookupEnvMutex.RUnlock() 111 | copiedInvocations := map[string][][]interface{}{} 112 | for key, value := range fake.invocations { 113 | copiedInvocations[key] = value 114 | } 115 | return copiedInvocations 116 | } 117 | 118 | func (fake *FakeImpl) recordInvocation(key string, args []interface{}) { 119 | fake.invocationsMutex.Lock() 120 | defer fake.invocationsMutex.Unlock() 121 | if fake.invocations == nil { 122 | fake.invocations = map[string][][]interface{}{} 123 | } 124 | if fake.invocations[key] == nil { 125 | fake.invocations[key] = [][]interface{}{} 126 | } 127 | fake.invocations[key] = append(fake.invocations[key], args) 128 | } 129 | -------------------------------------------------------------------------------- /mage/dependency.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package mage 18 | 19 | import ( 20 | "fmt" 21 | "log" 22 | 23 | "github.com/blang/semver/v4" 24 | "github.com/uwu-tools/magex/pkg" 25 | "github.com/uwu-tools/magex/shx" 26 | ) 27 | 28 | const ( 29 | // zeitgeist. 30 | defaultZeitgeistVersion = "v0.5.4" 31 | zeitgeistCmd = "zeitgeist" 32 | zeitgeistModule = "sigs.k8s.io/zeitgeist" 33 | zeitgeistRemoteModule = "sigs.k8s.io/zeitgeist/remote/zeitgeist" 34 | ) 35 | 36 | // Ensure zeitgeist is installed and on the PATH. 37 | func EnsureZeitgeist(version string) error { 38 | if version == "" { 39 | log.Printf( 40 | "A zeitgeist version to install was not specified. Using default version: %s", 41 | defaultZeitgeistVersion, 42 | ) 43 | 44 | version = defaultZeitgeistVersion 45 | } 46 | 47 | if _, err := semver.ParseTolerant(version); err != nil { 48 | return fmt.Errorf( 49 | "%s was not SemVer-compliant, cannot continue: %w", 50 | version, err, 51 | ) 52 | } 53 | 54 | if err := pkg.EnsurePackageWith(pkg.EnsurePackageOptions{ 55 | Name: zeitgeistModule, 56 | DefaultVersion: version, 57 | VersionCommand: "version", 58 | }); err != nil { 59 | return fmt.Errorf("ensuring package: %w", err) 60 | } 61 | 62 | return nil 63 | } 64 | 65 | // Ensure zeitgeist remote is installed and on the PATH. 66 | func EnsureZeitgeistRemote(version string) error { 67 | if version == "" { 68 | log.Printf( 69 | "A zeitgeist remote version to install was not specified. Using default version: %s", 70 | defaultZeitgeistVersion, 71 | ) 72 | 73 | version = defaultZeitgeistVersion 74 | } 75 | 76 | if _, err := semver.ParseTolerant(version); err != nil { 77 | return fmt.Errorf( 78 | "%s was not SemVer-compliant, cannot continue: %w", 79 | version, err, 80 | ) 81 | } 82 | 83 | if err := pkg.EnsurePackageWith(pkg.EnsurePackageOptions{ 84 | Name: zeitgeistRemoteModule, 85 | DefaultVersion: version, 86 | VersionCommand: "version", 87 | }); err != nil { 88 | return fmt.Errorf("ensuring package: %w", err) 89 | } 90 | 91 | return nil 92 | } 93 | 94 | // VerifyDeps runs zeitgeist to verify dependency versions. 95 | func VerifyDeps(version, basePath, configPath string, localOnly bool) error { 96 | if err := EnsureZeitgeist(version); err != nil { 97 | return fmt.Errorf("ensuring zeitgeist is installed: %w", err) 98 | } 99 | 100 | args := []string{"validate"} 101 | if localOnly { 102 | args = append(args, "--local-only") 103 | } 104 | 105 | if basePath != "" { 106 | args = append(args, "--base-path", basePath) 107 | } 108 | 109 | if configPath != "" { 110 | args = append(args, "--config", configPath) 111 | } 112 | 113 | if err := shx.RunV(zeitgeistCmd, args...); err != nil { 114 | return fmt.Errorf("running zeitgeist: %w", err) 115 | } 116 | 117 | return nil 118 | } 119 | 120 | /* 121 | ##@ Dependencies 122 | 123 | .SILENT: update-deps update-deps-go update-mocks 124 | .PHONY: update-deps update-deps-go update-mocks 125 | 126 | update-deps: update-deps-go ## Update all dependencies for this repo 127 | echo -e "${COLOR}Commit/PR the following changes:${NOCOLOR}" 128 | git status --short 129 | 130 | update-deps-go: GO111MODULE=on 131 | update-deps-go: ## Update all golang dependencies for this repo 132 | go get -u -t ./... 133 | go mod tidy 134 | go mod verify 135 | $(MAKE) test-go-unit 136 | ./scripts/update-all.sh 137 | 138 | update-mocks: ## Update all generated mocks 139 | go generate ./... 140 | for f in $(shell find . -name fake_*.go); do \ 141 | cp scripts/boilerplate/boilerplate.generatego.txt tmp ;\ 142 | cat $$f >> tmp ;\ 143 | mv tmp $$f ;\ 144 | done 145 | */ 146 | -------------------------------------------------------------------------------- /mage/boilerplate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package mage 18 | 19 | import ( 20 | "fmt" 21 | "log" 22 | "net/url" 23 | "os" 24 | "path" 25 | "path/filepath" 26 | "strings" 27 | 28 | "github.com/blang/semver/v4" 29 | "github.com/uwu-tools/magex/shx" 30 | 31 | kpath "k8s.io/utils/path" 32 | 33 | "sigs.k8s.io/release-utils/command" 34 | ) 35 | 36 | const ( 37 | // repo-infra (used for boilerplate script). 38 | defaultRepoInfraVersion = "v0.2.5" 39 | repoInfraURLBase = "https://raw.githubusercontent.com/kubernetes/repo-infra" 40 | ) 41 | 42 | // EnsureBoilerplateScript downloads copyright header boilerplate script, if 43 | // not already present in the repository. 44 | func EnsureBoilerplateScript(version, boilerplateScript string, forceInstall bool) error { 45 | found, err := kpath.Exists(kpath.CheckSymlinkOnly, boilerplateScript) 46 | if err != nil { 47 | return fmt.Errorf( 48 | "checking if copyright header boilerplate script (%s) exists: %w", 49 | boilerplateScript, err, 50 | ) 51 | } 52 | 53 | if !found || forceInstall { 54 | if version == "" { 55 | log.Printf( 56 | "A verify_boilerplate.py version to install was not specified. Using default version: %s", 57 | defaultRepoInfraVersion, 58 | ) 59 | 60 | version = defaultRepoInfraVersion 61 | } 62 | 63 | if !strings.HasPrefix(version, "v") { 64 | return fmt.Errorf( 65 | "repo-infra version (%s) must begin with a 'v'", 66 | version, 67 | ) 68 | } 69 | 70 | if _, err := semver.ParseTolerant(version); err != nil { 71 | return fmt.Errorf( 72 | "%s was not SemVer-compliant. Cannot continue.: %w", 73 | version, err, 74 | ) 75 | } 76 | 77 | binDir := filepath.Dir(boilerplateScript) 78 | if err := os.MkdirAll(binDir, 0o755); err != nil { 79 | return fmt.Errorf("creating binary directory: %w", err) 80 | } 81 | 82 | file, err := os.Create(boilerplateScript) 83 | if err != nil { 84 | return fmt.Errorf("creating file: %w", err) 85 | } 86 | 87 | defer file.Close() 88 | 89 | installURL, err := url.Parse(repoInfraURLBase) 90 | if err != nil { 91 | return fmt.Errorf("parsing URL: %w", err) 92 | } 93 | 94 | installURL.Path = path.Join( 95 | installURL.Path, 96 | version, 97 | "hack", 98 | "verify_boilerplate.py", 99 | ) 100 | 101 | installCmd := command.New( 102 | "curl", 103 | "-sSfL", 104 | installURL.String(), 105 | "-o", 106 | boilerplateScript, 107 | ) 108 | 109 | err = installCmd.RunSuccess() 110 | if err != nil { 111 | return fmt.Errorf("installing verify_boilerplate.py: %w", err) 112 | } 113 | } 114 | 115 | if err := os.Chmod(boilerplateScript, 0o755); err != nil { 116 | return fmt.Errorf("making script executable: %w", err) 117 | } 118 | 119 | return nil 120 | } 121 | 122 | // VerifyBoilerplate runs copyright header checks. 123 | func VerifyBoilerplate(version, binDir, boilerplateDir string, forceInstall bool) error { 124 | if _, err := kpath.Exists(kpath.CheckSymlinkOnly, boilerplateDir); err != nil { 125 | return fmt.Errorf( 126 | "checking if copyright header boilerplate directory (%s) exists: %w", 127 | boilerplateDir, err, 128 | ) 129 | } 130 | 131 | boilerplateScript := filepath.Join(binDir, "verify_boilerplate.py") 132 | 133 | if err := EnsureBoilerplateScript(version, boilerplateScript, forceInstall); err != nil { 134 | return fmt.Errorf("ensuring copyright header script is installed: %w", err) 135 | } 136 | 137 | if err := shx.RunV( 138 | boilerplateScript, 139 | "--boilerplate-dir", 140 | boilerplateDir, 141 | ); err != nil { 142 | return fmt.Errorf("running copyright header checks: %w", err) 143 | } 144 | 145 | return nil 146 | } 147 | -------------------------------------------------------------------------------- /hash/hash_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package hash_test 18 | 19 | import ( 20 | "crypto/sha1" //nolint: gosec 21 | "crypto/sha256" 22 | "hash" 23 | "os" 24 | "testing" 25 | 26 | "github.com/stretchr/testify/require" 27 | 28 | kHash "sigs.k8s.io/release-utils/hash" 29 | ) 30 | 31 | func TestSHA512ForFile(t *testing.T) { 32 | for _, tc := range []struct { 33 | prepare func() string 34 | expected string 35 | shouldError bool 36 | }{ 37 | { // success 38 | prepare: func() string { 39 | f, err := os.CreateTemp(t.TempDir(), "") 40 | require.NoError(t, err) 41 | 42 | _, err = f.WriteString("test") 43 | require.NoError(t, err) 44 | 45 | return f.Name() 46 | }, 47 | expected: "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f88" + 48 | "19a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc" + 49 | "5fa9ad8e6f57f50028a8ff", 50 | shouldError: false, 51 | }, 52 | { // error open file 53 | prepare: func() string { return "" }, 54 | shouldError: true, 55 | }, 56 | } { 57 | filename := tc.prepare() 58 | 59 | res, err := kHash.SHA512ForFile(filename) 60 | 61 | if tc.shouldError { 62 | require.Error(t, err) 63 | } else { 64 | require.NoError(t, err) 65 | require.Equal(t, tc.expected, res) 66 | } 67 | } 68 | } 69 | 70 | func TestSHA256ForFile(t *testing.T) { 71 | for _, tc := range []struct { 72 | prepare func() string 73 | expected string 74 | shouldError bool 75 | }{ 76 | { // success 77 | prepare: func() string { 78 | f, err := os.CreateTemp(t.TempDir(), "") 79 | require.NoError(t, err) 80 | 81 | _, err = f.WriteString("test") 82 | require.NoError(t, err) 83 | 84 | return f.Name() 85 | }, 86 | expected: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", 87 | shouldError: false, 88 | }, 89 | { // error open file 90 | prepare: func() string { return "" }, 91 | shouldError: true, 92 | }, 93 | } { 94 | filename := tc.prepare() 95 | 96 | res, err := kHash.SHA256ForFile(filename) 97 | 98 | if tc.shouldError { 99 | require.Error(t, err) 100 | } else { 101 | require.NoError(t, err) 102 | require.Equal(t, tc.expected, res) 103 | } 104 | } 105 | } 106 | 107 | func TestSHA1ForFile(t *testing.T) { 108 | for _, tc := range []struct { 109 | prepare func() string 110 | expected string 111 | shouldError bool 112 | }{ 113 | { // success 114 | prepare: func() string { 115 | f, err := os.CreateTemp(t.TempDir(), "") 116 | require.NoError(t, err) 117 | 118 | _, err = f.WriteString("test") 119 | require.NoError(t, err) 120 | 121 | return f.Name() 122 | }, 123 | expected: "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", 124 | shouldError: false, 125 | }, 126 | { // error open file 127 | prepare: func() string { return "" }, 128 | shouldError: true, 129 | }, 130 | } { 131 | filename := tc.prepare() 132 | 133 | res, err := kHash.SHA1ForFile(filename) 134 | 135 | if tc.shouldError { 136 | require.Error(t, err) 137 | } else { 138 | require.NoError(t, err) 139 | require.Equal(t, tc.expected, res) 140 | } 141 | } 142 | } 143 | 144 | func TestForFile(t *testing.T) { 145 | for _, tc := range []struct { 146 | prepare func() (string, hash.Hash) 147 | expected string 148 | shouldError bool 149 | }{ 150 | { // success 151 | prepare: func() (string, hash.Hash) { 152 | f, err := os.CreateTemp(t.TempDir(), "") 153 | require.NoError(t, err) 154 | 155 | _, err = f.WriteString("test") 156 | require.NoError(t, err) 157 | 158 | return f.Name(), sha1.New() //nolint: gosec 159 | }, 160 | expected: "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", 161 | shouldError: false, 162 | }, 163 | { // error hasher is nil 164 | prepare: func() (string, hash.Hash) { 165 | return "", nil 166 | }, 167 | shouldError: true, 168 | }, 169 | { // error file does not exist is nil 170 | prepare: func() (string, hash.Hash) { 171 | return "", sha256.New() 172 | }, 173 | shouldError: true, 174 | }, 175 | } { 176 | filename, hasher := tc.prepare() 177 | 178 | res, err := kHash.ForFile(filename, hasher) 179 | 180 | if tc.shouldError { 181 | require.Error(t, err) 182 | } else { 183 | require.NoError(t, err) 184 | require.Equal(t, tc.expected, res) 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "2" 3 | run: 4 | concurrency: 6 5 | linters: 6 | default: none 7 | enable: 8 | - asasalint 9 | - asciicheck 10 | - bidichk 11 | - bodyclose 12 | - canonicalheader 13 | - contextcheck 14 | - copyloopvar 15 | - decorder 16 | - dogsled 17 | - dupl 18 | - dupword 19 | - durationcheck 20 | - errcheck 21 | - errchkjson 22 | - errname 23 | - errorlint 24 | - exptostd 25 | - fatcontext 26 | - ginkgolinter 27 | - gocheckcompilerdirectives 28 | - gochecksumtype 29 | - goconst 30 | - gocritic 31 | - gocyclo 32 | - godot 33 | - godox 34 | - goheader 35 | - gomoddirectives 36 | - gomodguard 37 | - goprintffuncname 38 | - gosec 39 | - gosmopolitan 40 | - govet 41 | - grouper 42 | - iface 43 | - importas 44 | - ineffassign 45 | - intrange 46 | - loggercheck 47 | - makezero 48 | - mirror 49 | - misspell 50 | - musttag 51 | - nakedret 52 | - nilnesserr 53 | - nlreturn 54 | - nolintlint 55 | - nosprintfhostport 56 | - perfsprint 57 | - prealloc 58 | - predeclared 59 | - promlinter 60 | - protogetter 61 | - reassign 62 | - recvcheck 63 | - revive 64 | - rowserrcheck 65 | - sloglint 66 | - spancheck 67 | - sqlclosecheck 68 | - staticcheck 69 | - tagalign 70 | - testableexamples 71 | - testifylint 72 | - unconvert 73 | - unparam 74 | - unused 75 | - usestdlibvars 76 | - usetesting 77 | - whitespace 78 | - wsl_v5 79 | - zerologlint 80 | # - containedctx 81 | # - cyclop 82 | # - depguard 83 | # - err113 84 | # - exhaustive 85 | # - exhaustruct 86 | # - forbidigo 87 | # - forcetypeassert 88 | # - funlen 89 | # - gochecknoglobals 90 | # - gochecknoinits 91 | # - gocognit 92 | # - inamedparam 93 | # - interfacebloat 94 | # - ireturn 95 | # - lll 96 | # - maintidx 97 | # - mnd 98 | # - nestif 99 | # - nilerr 100 | # - nilnil 101 | # - noctx 102 | # - nonamedreturns 103 | # - paralleltest 104 | # - tagliatelle 105 | # - testpackage 106 | # - thelper 107 | # - tparallel 108 | # - varnamelen 109 | # - wastedassign 110 | # - wrapcheck 111 | settings: 112 | gocritic: 113 | enabled-checks: 114 | - appendCombine 115 | - badLock 116 | - badRegexp 117 | - badSorting 118 | - badSyncOnceFunc 119 | - boolExprSimplify 120 | - builtinShadow 121 | - builtinShadowDecl 122 | - commentedOutCode 123 | - commentedOutImport 124 | - deferInLoop 125 | - deferUnlambda 126 | - docStub 127 | - dupImport 128 | - dynamicFmtString 129 | - emptyDecl 130 | - emptyFallthrough 131 | - emptyStringTest 132 | - equalFold 133 | - evalOrder 134 | - exposedSyncMutex 135 | - externalErrorReassign 136 | - filepathJoin 137 | - hexLiteral 138 | - httpNoBody 139 | - hugeParam 140 | - importShadow 141 | - indexAlloc 142 | - initClause 143 | - methodExprCall 144 | - nestingReduce 145 | - nilValReturn 146 | - octalLiteral 147 | - paramTypeCombine 148 | - preferDecodeRune 149 | - preferFilepathJoin 150 | - preferFprint 151 | - preferStringWriter 152 | - preferWriteByte 153 | - ptrToRefParam 154 | - rangeAppendAll 155 | - rangeExprCopy 156 | - rangeValCopy 157 | - redundantSprint 158 | - regexpPattern 159 | - regexpSimplify 160 | - returnAfterHttpError 161 | - ruleguard 162 | - sliceClear 163 | - sloppyReassign 164 | - sortSlice 165 | - sprintfQuotedString 166 | - sqlQuery 167 | - stringConcatSimplify 168 | - stringXbytes 169 | - stringsCompare 170 | - syncMapLoadAndDelete 171 | - timeExprSimplify 172 | - todoCommentWithoutDetail 173 | - tooManyResultsChecker 174 | - truncateCmp 175 | - typeAssertChain 176 | - typeDefFirst 177 | - typeUnparen 178 | - uncheckedInlineErr 179 | - unlabelStmt 180 | - unnamedResult 181 | - unnecessaryBlock 182 | - unnecessaryDefer 183 | - weakCond 184 | - yodaStyleExpr 185 | gocyclo: 186 | min-complexity: 40 187 | godox: 188 | keywords: 189 | - BUG 190 | - FIXME 191 | - HACK 192 | exclusions: 193 | generated: lax 194 | presets: 195 | - comments 196 | - common-false-positives 197 | - legacy 198 | - std-error-handling 199 | paths: 200 | - third_party$ 201 | - builtin$ 202 | - examples$ 203 | issues: 204 | max-issues-per-linter: 0 205 | max-same-issues: 0 206 | formatters: 207 | enable: 208 | - gci 209 | - gofmt 210 | - gofumpt 211 | - goimports 212 | settings: 213 | gci: 214 | sections: 215 | - standard 216 | - default 217 | - prefix(k8s.io) 218 | - prefix(sigs.k8s.io) 219 | - localmodule 220 | exclusions: 221 | generated: lax 222 | paths: 223 | - third_party$ 224 | - builtin$ 225 | - examples$ 226 | -------------------------------------------------------------------------------- /editor/editor.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package editor 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "io" 23 | "os" 24 | "os/exec" 25 | "path/filepath" 26 | "runtime" 27 | "strings" 28 | 29 | "github.com/sirupsen/logrus" 30 | ) 31 | 32 | const ( 33 | // sorry, blame Git 34 | // TODO: on Windows rely on 'start' to launch the editor associated 35 | // with the given file type. If we can't because of the need of 36 | // blocking, use a script with 'ftype' and 'assoc' to detect it. 37 | defaultEditor = "vi" 38 | defaultShell = "/bin/bash" 39 | windowsEditor = "notepad" 40 | windowsShell = "cmd" 41 | ) 42 | 43 | // Editor holds the command-line args to fire up the editor. 44 | type Editor struct { 45 | Args []string 46 | Shell bool 47 | } 48 | 49 | // NewDefaultEditor creates a struct Editor that uses the OS environment to 50 | // locate the editor program, looking at EDITOR environment variable to find 51 | // the proper command line. If the provided editor has no spaces, or no quotes, 52 | // it is treated as a bare command to be loaded. Otherwise, the string will 53 | // be passed to the user's shell for execution. 54 | func NewDefaultEditor(envs []string) Editor { 55 | args, shell := defaultEnvEditor(envs) 56 | 57 | return Editor{ 58 | Args: args, 59 | Shell: shell, 60 | } 61 | } 62 | 63 | func defaultEnvShell() []string { 64 | shell := os.Getenv("SHELL") 65 | if shell == "" { 66 | shell = platformize(defaultShell, windowsShell) 67 | } 68 | 69 | flag := "-c" 70 | if shell == windowsShell { 71 | flag = "/C" 72 | } 73 | 74 | return []string{shell, flag} 75 | } 76 | 77 | func defaultEnvEditor(envs []string) ([]string, bool) { 78 | var editor string 79 | 80 | for _, env := range envs { 81 | if env != "" { 82 | editor = os.Getenv(env) 83 | } 84 | 85 | if editor != "" { 86 | break 87 | } 88 | } 89 | 90 | if editor == "" { 91 | editor = platformize(defaultEditor, windowsEditor) 92 | } 93 | 94 | if !strings.Contains(editor, " ") { 95 | return []string{editor}, false 96 | } 97 | 98 | if !strings.ContainsAny(editor, "\"'\\") { 99 | return strings.Split(editor, " "), false 100 | } 101 | // rather than parse the shell arguments ourselves, punt to the shell 102 | shell := defaultEnvShell() 103 | 104 | return append(shell, editor), true 105 | } 106 | 107 | func (e Editor) args(path string) []string { 108 | args := make([]string, len(e.Args)) 109 | copy(args, e.Args) 110 | 111 | if e.Shell { 112 | last := args[len(args)-1] 113 | args[len(args)-1] = fmt.Sprintf("%s %q", last, path) 114 | } else { 115 | args = append(args, path) //nolint: makezero 116 | } 117 | 118 | return args 119 | } 120 | 121 | // Launch opens the described or returns an error. The TTY will be protected, and 122 | // SIGQUIT, SIGTERM, and SIGINT will all be trapped. 123 | func (e Editor) Launch(path string) error { 124 | if len(e.Args) == 0 { 125 | return fmt.Errorf("no editor defined, can't open %s", path) 126 | } 127 | 128 | abs, err := filepath.Abs(path) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | args := e.args(abs) 134 | // TODO: check to validate the args and maybe sabitize those 135 | cmd := exec.Command(args[0], args[1:]...) //nolint: gosec 136 | cmd.Stdout = os.Stdout 137 | cmd.Stderr = os.Stderr 138 | cmd.Stdin = os.Stdin 139 | 140 | logrus.Infof("Opening file with editor %v", args) 141 | 142 | if err := (TTY{In: os.Stdin, TryDev: true}).Safe(cmd.Run); err != nil { 143 | execErr := &exec.Error{} 144 | if errors.As(err, &execErr) && errors.Is(execErr.Err, exec.ErrNotFound) { 145 | return fmt.Errorf("unable to launch the editor %q", strings.Join(e.Args, " ")) 146 | } 147 | 148 | return fmt.Errorf("there was a problem with the editor %q: %w", strings.Join(e.Args, " "), err) 149 | } 150 | 151 | return nil 152 | } 153 | 154 | // LaunchTempFile reads the provided stream into a temporary file in the given directory 155 | // and file prefix, and then invokes Launch with the path of that file. It will return 156 | // the contents of the file after launch, any errors that occur, and the path of the 157 | // temporary file so the caller can clean it up as needed. 158 | func (e Editor) LaunchTempFile(prefix, suffix string, r io.Reader) (bytes []byte, path string, err error) { 159 | f, err := os.CreateTemp("", fmt.Sprintf("%s*%s", prefix, suffix)) 160 | if err != nil { 161 | return nil, "", err 162 | } 163 | 164 | defer f.Close() 165 | 166 | path = f.Name() 167 | if _, err := io.Copy(f, r); err != nil { 168 | os.Remove(path) 169 | 170 | return nil, path, err 171 | } 172 | // This file descriptor needs to close so the next process (Launch) can claim it. 173 | f.Close() 174 | 175 | if err := e.Launch(path); err != nil { 176 | return nil, path, err 177 | } 178 | 179 | bytes, err = os.ReadFile(path) 180 | 181 | return bytes, path, err 182 | } 183 | 184 | func platformize(linux, windows string) string { 185 | if runtime.GOOS == "windows" { 186 | return windows 187 | } 188 | 189 | return linux 190 | } 191 | -------------------------------------------------------------------------------- /helpers/tablewriter_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package helpers 18 | 19 | import ( 20 | "bytes" 21 | "os" 22 | "path/filepath" 23 | "testing" 24 | 25 | "github.com/olekukonko/tablewriter" 26 | "github.com/stretchr/testify/require" 27 | ) 28 | 29 | // compareGolden compares actual output with golden file. 30 | func compareGolden(t *testing.T, actual, goldenFile string) { 31 | t.Helper() 32 | 33 | goldenPath := filepath.Join("testdata", goldenFile) 34 | expected, err := os.ReadFile(goldenPath) 35 | require.NoError(t, err, "Failed to read golden file: %s", goldenPath) 36 | 37 | require.Equal(t, string(expected), actual, "Output doesn't match golden file: %s", goldenFile) 38 | } 39 | 40 | func TestNewTableWriter(t *testing.T) { 41 | t.Parallel() 42 | 43 | t.Run("NoOptions", func(t *testing.T) { 44 | t.Parallel() 45 | 46 | var output bytes.Buffer 47 | 48 | table := NewTableWriter(&output) 49 | 50 | require.NotNil(t, table) 51 | require.IsType(t, &tablewriter.Table{}, table) 52 | 53 | table.Header("Name", "Age") 54 | _ = table.Append([]string{"John", "30"}) 55 | _ = table.Render() 56 | 57 | compareGolden(t, output.String(), "no_options.golden") 58 | }) 59 | 60 | t.Run("WithSingleOption", func(t *testing.T) { 61 | t.Parallel() 62 | 63 | var output bytes.Buffer 64 | 65 | table := NewTableWriter(&output, tablewriter.WithMaxWidth(80)) 66 | 67 | require.NotNil(t, table) 68 | require.IsType(t, &tablewriter.Table{}, table) 69 | 70 | table.Header("Name", "Age") 71 | _ = table.Append([]string{"John", "30"}) 72 | _ = table.Render() 73 | 74 | compareGolden(t, output.String(), "with_single_option.golden") 75 | }) 76 | 77 | t.Run("WithMultipleOptions", func(t *testing.T) { 78 | t.Parallel() 79 | 80 | var output bytes.Buffer 81 | 82 | table := NewTableWriter(&output, 83 | tablewriter.WithHeader([]string{"Name", "Age"}), 84 | tablewriter.WithMaxWidth(80), 85 | ) 86 | 87 | require.NotNil(t, table) 88 | require.IsType(t, &tablewriter.Table{}, table) 89 | 90 | _ = table.Append([]string{"John", "30"}) 91 | _ = table.Render() 92 | 93 | compareGolden(t, output.String(), "with_multiple_options.golden") 94 | }) 95 | 96 | t.Run("WithHeaderOption", func(t *testing.T) { 97 | t.Parallel() 98 | 99 | var output bytes.Buffer 100 | 101 | table := NewTableWriter(&output, tablewriter.WithHeader([]string{"Name", "Age"})) 102 | 103 | require.NotNil(t, table) 104 | require.IsType(t, &tablewriter.Table{}, table) 105 | 106 | _ = table.Append([]string{"John", "30"}) 107 | _ = table.Render() 108 | 109 | compareGolden(t, output.String(), "with_header_option.golden") 110 | }) 111 | 112 | t.Run("WithFooterOption", func(t *testing.T) { 113 | t.Parallel() 114 | 115 | var output bytes.Buffer 116 | 117 | table := NewTableWriter(&output, tablewriter.WithFooter([]string{"Total", "1"})) 118 | 119 | require.NotNil(t, table) 120 | require.IsType(t, &tablewriter.Table{}, table) 121 | 122 | table.Header("Name", "Age") 123 | _ = table.Append([]string{"John", "30"}) 124 | _ = table.Render() 125 | 126 | compareGolden(t, output.String(), "with_footer_option.golden") 127 | }) 128 | 129 | t.Run("EmptyTable", func(t *testing.T) { 130 | t.Parallel() 131 | 132 | var output bytes.Buffer 133 | 134 | table := NewTableWriter(&output) 135 | 136 | require.NotNil(t, table) 137 | require.IsType(t, &tablewriter.Table{}, table) 138 | 139 | table.Header("Name", "Age") 140 | _ = table.Render() 141 | 142 | compareGolden(t, output.String(), "empty_table.golden") 143 | }) 144 | 145 | t.Run("MultipleRows", func(t *testing.T) { 146 | t.Parallel() 147 | 148 | var output bytes.Buffer 149 | 150 | table := NewTableWriter(&output) 151 | 152 | require.NotNil(t, table) 153 | require.IsType(t, &tablewriter.Table{}, table) 154 | 155 | table.Header("Name", "Age", "City") 156 | _ = table.Append([]string{"John", "30", "New York"}) 157 | _ = table.Append([]string{"Jane", "25", "Boston"}) 158 | _ = table.Append([]string{"Bob", "35", "Chicago"}) 159 | _ = table.Render() 160 | 161 | compareGolden(t, output.String(), "multiple_rows.golden") 162 | }) 163 | } 164 | 165 | func TestNewTableWriterWithDefaults(t *testing.T) { 166 | t.Parallel() 167 | 168 | t.Run("WithDefaults", func(t *testing.T) { 169 | t.Parallel() 170 | 171 | var output bytes.Buffer 172 | 173 | table := NewTableWriterWithDefaults(&output) 174 | 175 | require.NotNil(t, table) 176 | require.IsType(t, &tablewriter.Table{}, table) 177 | }) 178 | 179 | t.Run("WithDefaultsAndHeader", func(t *testing.T) { 180 | t.Parallel() 181 | 182 | var output bytes.Buffer 183 | 184 | header := []string{"TESTGRID BOARD", "TITLE", "STATUS", "STATUS DETAILS"} 185 | table := NewTableWriterWithDefaultsAndHeader(&output, header) 186 | 187 | require.NotNil(t, table) 188 | require.IsType(t, &tablewriter.Table{}, table) 189 | }) 190 | 191 | t.Run("WithDefaultsAndAdditionalOptions", func(t *testing.T) { 192 | t.Parallel() 193 | 194 | var output bytes.Buffer 195 | 196 | table := NewTableWriterWithDefaults(&output, tablewriter.WithMaxWidth(100)) 197 | 198 | require.NotNil(t, table) 199 | require.IsType(t, &tablewriter.Table{}, table) 200 | }) 201 | } 202 | -------------------------------------------------------------------------------- /http/agent_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package http_test 18 | 19 | import ( 20 | "errors" 21 | "net/http" 22 | "net/url" 23 | "testing" 24 | 25 | "github.com/stretchr/testify/assert" 26 | "github.com/stretchr/testify/require" 27 | 28 | rhttp "sigs.k8s.io/release-utils/http" 29 | "sigs.k8s.io/release-utils/http/httpfakes" 30 | ) 31 | 32 | func TestGetRequest(t *testing.T) { 33 | for _, tc := range map[string]struct { 34 | prepare func(*httpfakes.FakeAgentImplementation) 35 | assert func(*http.Response, error) 36 | }{ 37 | "should succeed": { 38 | prepare: func(mock *httpfakes.FakeAgentImplementation) { 39 | mock.SendGetRequestReturns(&http.Response{StatusCode: http.StatusOK}, nil) 40 | }, 41 | assert: func(response *http.Response, err error) { 42 | require.NoError(t, err) 43 | assert.Equal(t, http.StatusOK, response.StatusCode) 44 | }, 45 | }, 46 | "should succeed on retry": { 47 | prepare: func(mock *httpfakes.FakeAgentImplementation) { 48 | mock.SendGetRequestReturnsOnCall(0, &http.Response{StatusCode: http.StatusInternalServerError}, nil) 49 | mock.SendGetRequestReturnsOnCall(1, &http.Response{StatusCode: http.StatusOK}, nil) 50 | }, 51 | assert: func(response *http.Response, err error) { 52 | require.NoError(t, err) 53 | assert.Equal(t, http.StatusOK, response.StatusCode) 54 | }, 55 | }, 56 | "should retry on internal server error": { 57 | prepare: func(mock *httpfakes.FakeAgentImplementation) { 58 | mock.SendGetRequestReturns(&http.Response{StatusCode: http.StatusInternalServerError}, nil) 59 | }, 60 | assert: func(response *http.Response, err error) { 61 | require.Error(t, err) 62 | assert.NotNil(t, response) 63 | }, 64 | }, 65 | "should retry on too many requests": { 66 | prepare: func(mock *httpfakes.FakeAgentImplementation) { 67 | mock.SendGetRequestReturns(&http.Response{StatusCode: http.StatusTooManyRequests}, nil) 68 | }, 69 | assert: func(response *http.Response, err error) { 70 | require.Error(t, err) 71 | assert.NotNil(t, response) 72 | }, 73 | }, 74 | "should retry on URL error": { 75 | prepare: func(mock *httpfakes.FakeAgentImplementation) { 76 | mock.SendGetRequestReturns(nil, &url.Error{Err: errors.New("test")}) 77 | }, 78 | assert: func(response *http.Response, err error) { 79 | require.Error(t, err) 80 | require.Contains(t, err.Error(), "test") 81 | assert.Nil(t, response) 82 | }, 83 | }, 84 | } { 85 | agent := rhttp.NewAgent().WithWaitTime(0) 86 | mock := &httpfakes.FakeAgentImplementation{} 87 | agent.SetImplementation(mock) 88 | 89 | if tc.prepare != nil { 90 | tc.prepare(mock) 91 | } 92 | 93 | //nolint:bodyclose // no need to close for mocked tests 94 | tc.assert(agent.GetRequest("")) 95 | } 96 | } 97 | 98 | func TestPostRequest(t *testing.T) { 99 | for _, tc := range map[string]struct { 100 | prepare func(*httpfakes.FakeAgentImplementation) 101 | assert func(*http.Response, error) 102 | }{ 103 | "should succeed": { 104 | prepare: func(mock *httpfakes.FakeAgentImplementation) { 105 | mock.SendPostRequestReturns(&http.Response{StatusCode: http.StatusOK}, nil) 106 | }, 107 | assert: func(response *http.Response, err error) { 108 | require.NoError(t, err) 109 | assert.Equal(t, http.StatusOK, response.StatusCode) 110 | }, 111 | }, 112 | "should succeed on retry": { 113 | prepare: func(mock *httpfakes.FakeAgentImplementation) { 114 | mock.SendPostRequestReturnsOnCall(0, &http.Response{StatusCode: http.StatusInternalServerError}, nil) 115 | mock.SendPostRequestReturnsOnCall(1, &http.Response{StatusCode: http.StatusOK}, nil) 116 | }, 117 | assert: func(response *http.Response, err error) { 118 | require.NoError(t, err) 119 | assert.Equal(t, http.StatusOK, response.StatusCode) 120 | }, 121 | }, 122 | "should retry on internal server error": { 123 | prepare: func(mock *httpfakes.FakeAgentImplementation) { 124 | mock.SendPostRequestReturns(&http.Response{StatusCode: http.StatusInternalServerError}, nil) 125 | }, 126 | assert: func(response *http.Response, err error) { 127 | require.Error(t, err) 128 | assert.NotNil(t, response) 129 | }, 130 | }, 131 | "should retry on too many requests": { 132 | prepare: func(mock *httpfakes.FakeAgentImplementation) { 133 | mock.SendPostRequestReturns(&http.Response{StatusCode: http.StatusTooManyRequests}, nil) 134 | }, 135 | assert: func(response *http.Response, err error) { 136 | require.Error(t, err) 137 | assert.NotNil(t, response) 138 | }, 139 | }, 140 | "should retry on URL error": { 141 | prepare: func(mock *httpfakes.FakeAgentImplementation) { 142 | mock.SendPostRequestReturns(nil, &url.Error{Err: errors.New("test")}) 143 | }, 144 | assert: func(response *http.Response, err error) { 145 | require.Error(t, err) 146 | require.Contains(t, err.Error(), "test") 147 | assert.Nil(t, response) 148 | }, 149 | }, 150 | } { 151 | agent := rhttp.NewAgent().WithWaitTime(0) 152 | mock := &httpfakes.FakeAgentImplementation{} 153 | agent.SetImplementation(mock) 154 | 155 | if tc.prepare != nil { 156 | tc.prepare(mock) 157 | } 158 | 159 | //nolint:bodyclose // no need to close for mocked tests 160 | tc.assert(agent.PostRequest("", nil)) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /mage/golangci-lint.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package mage 18 | 19 | import ( 20 | "fmt" 21 | "log" 22 | "net/url" 23 | "os" 24 | "path" 25 | "path/filepath" 26 | "strings" 27 | 28 | "github.com/blang/semver/v4" 29 | "github.com/uwu-tools/magex/pkg" 30 | "github.com/uwu-tools/magex/pkg/gopath" 31 | "github.com/uwu-tools/magex/shx" 32 | 33 | kpath "k8s.io/utils/path" 34 | 35 | "sigs.k8s.io/release-utils/command" 36 | "sigs.k8s.io/release-utils/env" 37 | ) 38 | 39 | const ( 40 | // golangci-lint. 41 | defaultGolangCILintVersion = "v2.5.0" 42 | golangciCmd = "golangci-lint" 43 | golangciConfig = ".golangci.yml" 44 | golangciURLBase = "https://raw.githubusercontent.com/golangci/golangci-lint" 45 | defaultMinGoVersion = "1.24" 46 | ) 47 | 48 | // Ensure golangci-lint is installed and on the PATH. 49 | func EnsureGolangCILint(version string, forceInstall bool) error { 50 | found, err := pkg.IsCommandAvailable(golangciCmd, "--version", version) 51 | if err != nil { 52 | return fmt.Errorf( 53 | "checking if %s is available: %w", 54 | golangciCmd, err, 55 | ) 56 | } 57 | 58 | if !found || forceInstall { 59 | if version == "" { 60 | log.Printf( 61 | "A golangci-lint version to install was not specified. Using default version: %s", 62 | defaultGolangCILintVersion, 63 | ) 64 | 65 | version = defaultGolangCILintVersion 66 | } 67 | 68 | if !strings.HasPrefix(version, "v") { 69 | return fmt.Errorf( 70 | "golangci-lint version (%s) must begin with a 'v'", 71 | version, 72 | ) 73 | } 74 | 75 | if _, err := semver.ParseTolerant(version); err != nil { 76 | return fmt.Errorf( 77 | "%s was not SemVer-compliant. Cannot continue.: %w", 78 | version, err, 79 | ) 80 | } 81 | 82 | installURL, err := url.Parse(golangciURLBase) 83 | if err != nil { 84 | return fmt.Errorf("parsing URL: %w", err) 85 | } 86 | 87 | installURL.Path = path.Join(installURL.Path, version, "install.sh") 88 | 89 | err = gopath.EnsureGopathBin() 90 | if err != nil { 91 | return fmt.Errorf("ensuring $GOPATH/bin: %w", err) 92 | } 93 | 94 | gopathBin := gopath.GetGopathBin() 95 | 96 | installCmd := command.New( 97 | "curl", 98 | "-sSfL", 99 | installURL.String(), 100 | ).Pipe( 101 | "sh", 102 | "-s", 103 | "--", 104 | "-b", 105 | gopathBin, 106 | version, 107 | ) 108 | 109 | err = installCmd.RunSuccess() 110 | if err != nil { 111 | return fmt.Errorf("installing golangci-lint: %w", err) 112 | } 113 | } 114 | 115 | return nil 116 | } 117 | 118 | // RunGolangCILint runs all golang linters. 119 | func RunGolangCILint(version string, forceInstall bool, args ...string) error { 120 | if _, err := kpath.Exists(kpath.CheckSymlinkOnly, golangciConfig); err != nil { 121 | return fmt.Errorf( 122 | "checking if golangci-lint config file (%s) exists: %w", 123 | golangciConfig, err, 124 | ) 125 | } 126 | 127 | if err := EnsureGolangCILint(version, forceInstall); err != nil { 128 | return fmt.Errorf("ensuring golangci-lint is installed: %w", err) 129 | } 130 | 131 | if err := shx.RunV(golangciCmd, "version"); err != nil { 132 | return fmt.Errorf("getting golangci-lint version: %w", err) 133 | } 134 | 135 | if err := shx.RunV(golangciCmd, "linters"); err != nil { 136 | return fmt.Errorf("listing golangci-lint linters: %w", err) 137 | } 138 | 139 | runArgs := []string{"run"} 140 | runArgs = append(runArgs, args...) 141 | 142 | if err := shx.RunV(golangciCmd, runArgs...); err != nil { 143 | return fmt.Errorf("running golangci-lint linters: %w", err) 144 | } 145 | 146 | return nil 147 | } 148 | 149 | func TestGo(verbose bool, pkgs ...string) error { 150 | return testGo(verbose, "", pkgs...) 151 | } 152 | 153 | func TestGoWithTags(verbose bool, tags string, pkgs ...string) error { 154 | return testGo(verbose, tags, pkgs...) 155 | } 156 | 157 | func testGo(verbose bool, tags string, pkgs ...string) error { 158 | verboseFlag := "" 159 | if verbose { 160 | verboseFlag = "-v" 161 | } 162 | 163 | pkgArgs := []string{} 164 | 165 | if len(pkgs) > 0 { 166 | for _, p := range pkgs { 167 | pkgArg := fmt.Sprintf("./%s/...", p) 168 | pkgArgs = append(pkgArgs, pkgArg) 169 | } 170 | } else { 171 | pkgArgs = []string{"./..."} 172 | } 173 | 174 | cmdArgs := []string{"test"} 175 | cmdArgs = append(cmdArgs, verboseFlag) 176 | 177 | if tags != "" { 178 | cmdArgs = append(cmdArgs, "-tags", tags) 179 | } 180 | 181 | cmdArgs = append(cmdArgs, pkgArgs...) 182 | 183 | if err := shx.RunV( 184 | "go", 185 | cmdArgs..., 186 | ); err != nil { 187 | return fmt.Errorf("running go test: %w", err) 188 | } 189 | 190 | return nil 191 | } 192 | 193 | // VerifyGoMod runs `go mod tidy` and `git diff --exit-code go.*` to ensure 194 | // all module updates have been checked in. 195 | func VerifyGoMod() error { 196 | minGoVersion := env.Default("MIN_GO_VERSION", defaultMinGoVersion) 197 | if err := shx.RunV( 198 | "go", "mod", "tidy", "-compat="+minGoVersion, 199 | ); err != nil { 200 | return fmt.Errorf("running go mod tidy: %w", err) 201 | } 202 | 203 | if err := shx.RunV("git", "diff", "--exit-code", "go.*"); err != nil { 204 | return fmt.Errorf("running go mod tidy: %w", err) 205 | } 206 | 207 | return nil 208 | } 209 | 210 | // VerifyBuild builds the project for a chosen set of platforms. 211 | func VerifyBuild(scriptDir string) error { 212 | wd, err := os.Getwd() 213 | if err != nil { 214 | return fmt.Errorf("getting working directory: %w", err) 215 | } 216 | 217 | scriptDir = filepath.Join(wd, scriptDir) 218 | 219 | buildScript := filepath.Join(scriptDir, "verify-build.sh") 220 | if err := shx.RunV(buildScript); err != nil { 221 | return fmt.Errorf("running go build: %w", err) 222 | } 223 | 224 | return nil 225 | } 226 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package version 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "os" 23 | "runtime" 24 | "runtime/debug" 25 | "strings" 26 | "sync" 27 | "text/tabwriter" 28 | "time" 29 | 30 | "github.com/common-nighthawk/go-figure" 31 | ) 32 | 33 | const unknown = "unknown" 34 | 35 | // Base version information. 36 | // 37 | // This is the fallback data used when version information from git is not 38 | // provided via go ldflags. 39 | var ( 40 | // Output of "git describe". The prerequisite is that the 41 | // branch should be tagged using the correct versioning strategy. 42 | gitVersion = "devel" 43 | // SHA1 from git, output of $(git rev-parse HEAD). 44 | gitCommit = unknown 45 | // State of git tree, either "clean" or "dirty". 46 | gitTreeState = unknown 47 | // Build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ'). 48 | buildDate = unknown 49 | // flag to print the ascii name banner. 50 | asciiName = "true" 51 | // goVersion is the used golang version. 52 | goVersion = unknown 53 | // compiler is the used golang compiler. 54 | compiler = unknown 55 | // platform is the used os/arch identifier. 56 | platform = unknown 57 | 58 | once sync.Once 59 | info = Info{} 60 | ) 61 | 62 | type Info struct { 63 | GitVersion string `json:"gitVersion"` 64 | GitCommit string `json:"gitCommit"` 65 | GitTreeState string `json:"gitTreeState"` 66 | BuildDate string `json:"buildDate"` 67 | GoVersion string `json:"goVersion"` 68 | Compiler string `json:"compiler"` 69 | Platform string `json:"platform"` 70 | 71 | ASCIIName string `json:"-"` 72 | FontName string `json:"-"` 73 | Name string `json:"-"` 74 | Description string `json:"-"` 75 | } 76 | 77 | func getBuildInfo() *debug.BuildInfo { 78 | bi, ok := debug.ReadBuildInfo() 79 | if !ok { 80 | return nil 81 | } 82 | 83 | return bi 84 | } 85 | 86 | func getGitVersion(bi *debug.BuildInfo) string { 87 | if bi == nil { 88 | return unknown 89 | } 90 | 91 | // TODO: remove this when the issue https://github.com/golang/go/issues/29228 is fixed 92 | if bi.Main.Version == "(devel)" || bi.Main.Version == "" { 93 | return gitVersion 94 | } 95 | 96 | return bi.Main.Version 97 | } 98 | 99 | func getCommit(bi *debug.BuildInfo) string { 100 | return getKey(bi, "vcs.revision") 101 | } 102 | 103 | func getDirty(bi *debug.BuildInfo) string { 104 | modified := getKey(bi, "vcs.modified") 105 | if modified == "true" { 106 | return "dirty" 107 | } 108 | 109 | if modified == "false" { 110 | return "clean" 111 | } 112 | 113 | return unknown 114 | } 115 | 116 | func getBuildDate(bi *debug.BuildInfo) string { 117 | buildTime := getKey(bi, "vcs.time") 118 | 119 | t, err := time.Parse("2006-01-02T15:04:05Z", buildTime) 120 | if err != nil { 121 | return unknown 122 | } 123 | 124 | return t.Format("2006-01-02T15:04:05") 125 | } 126 | 127 | func getKey(bi *debug.BuildInfo, key string) string { 128 | if bi == nil { 129 | return unknown 130 | } 131 | 132 | for _, iter := range bi.Settings { 133 | if iter.Key == key { 134 | return iter.Value 135 | } 136 | } 137 | 138 | return unknown 139 | } 140 | 141 | // GetVersionInfo represents known information on how this binary was built. 142 | func GetVersionInfo() Info { 143 | once.Do(func() { 144 | buildInfo := getBuildInfo() 145 | gitVersion = getGitVersion(buildInfo) 146 | 147 | if gitCommit == unknown { 148 | gitCommit = getCommit(buildInfo) 149 | } 150 | 151 | if gitTreeState == unknown { 152 | gitTreeState = getDirty(buildInfo) 153 | } 154 | 155 | if buildDate == unknown { 156 | buildDate = getBuildDate(buildInfo) 157 | } 158 | 159 | if goVersion == unknown { 160 | goVersion = runtime.Version() 161 | } 162 | 163 | if compiler == unknown { 164 | compiler = runtime.Compiler 165 | } 166 | 167 | if platform == unknown { 168 | platform = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) 169 | } 170 | 171 | info = Info{ 172 | ASCIIName: asciiName, 173 | GitVersion: gitVersion, 174 | GitCommit: gitCommit, 175 | GitTreeState: gitTreeState, 176 | BuildDate: buildDate, 177 | GoVersion: goVersion, 178 | Compiler: compiler, 179 | Platform: platform, 180 | } 181 | }) 182 | 183 | return info 184 | } 185 | 186 | // String returns the string representation of the version info. 187 | func (i *Info) String() string { 188 | b := strings.Builder{} 189 | w := tabwriter.NewWriter(&b, 0, 0, 2, ' ', 0) 190 | 191 | // name and description are optional. 192 | if i.Name != "" { 193 | if i.ASCIIName == "true" { 194 | f := figure.NewFigure(strings.ToUpper(i.Name), i.FontName, true) 195 | _, _ = fmt.Fprint(w, f.String()) 196 | } 197 | 198 | _, _ = fmt.Fprint(w, i.Name) 199 | if i.Description != "" { 200 | _, _ = fmt.Fprintf(w, ": %s", i.Description) 201 | } 202 | 203 | _, _ = fmt.Fprint(w, "\n\n") 204 | } 205 | 206 | _, _ = fmt.Fprintf(w, "GitVersion:\t%s\n", i.GitVersion) 207 | _, _ = fmt.Fprintf(w, "GitCommit:\t%s\n", i.GitCommit) 208 | _, _ = fmt.Fprintf(w, "GitTreeState:\t%s\n", i.GitTreeState) 209 | _, _ = fmt.Fprintf(w, "BuildDate:\t%s\n", i.BuildDate) 210 | _, _ = fmt.Fprintf(w, "GoVersion:\t%s\n", i.GoVersion) 211 | _, _ = fmt.Fprintf(w, "Compiler:\t%s\n", i.Compiler) 212 | _, _ = fmt.Fprintf(w, "Platform:\t%s\n", i.Platform) 213 | 214 | _ = w.Flush() 215 | 216 | return b.String() 217 | } 218 | 219 | // JSONString returns the JSON representation of the version info. 220 | func (i *Info) JSONString() (string, error) { 221 | b, err := json.MarshalIndent(i, "", " ") 222 | if err != nil { 223 | return "", err 224 | } 225 | 226 | return string(b), nil 227 | } 228 | 229 | func (i *Info) CheckFontName(fontName string) bool { 230 | assetNames := figure.AssetNames() 231 | 232 | for _, font := range assetNames { 233 | if strings.Contains(font, fontName) { 234 | return true 235 | } 236 | } 237 | 238 | fmt.Fprintln(os.Stderr, "font not valid, using default") 239 | 240 | return false 241 | } 242 | -------------------------------------------------------------------------------- /tar/tar_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tar 18 | 19 | import ( 20 | "archive/tar" 21 | "io" 22 | "os" 23 | "path/filepath" 24 | "regexp" 25 | "testing" 26 | 27 | "github.com/sirupsen/logrus" 28 | "github.com/stretchr/testify/require" 29 | ) 30 | 31 | func TestCompress(t *testing.T) { 32 | baseTmpDir := t.TempDir() 33 | 34 | for _, fileName := range []string{ 35 | "1.txt", "2.bin", "3.md", 36 | } { 37 | require.NoError(t, os.WriteFile( 38 | filepath.Join(baseTmpDir, fileName), 39 | []byte{1, 2, 3}, 40 | os.FileMode(0o644), 41 | )) 42 | } 43 | 44 | subTmpDir := filepath.Join(baseTmpDir, "sub") 45 | require.NoError(t, os.MkdirAll(subTmpDir, os.FileMode(0o755))) 46 | 47 | for _, fileName := range []string{ 48 | "4.txt", "5.bin", "6.md", 49 | } { 50 | require.NoError(t, os.WriteFile( 51 | filepath.Join(subTmpDir, fileName), 52 | []byte{4, 5, 6}, 53 | os.FileMode(0o644), 54 | )) 55 | } 56 | 57 | logrus.SetLevel(logrus.DebugLevel) 58 | 59 | require.NoError(t, os.Symlink( 60 | filepath.Join(baseTmpDir, "1.txt"), 61 | filepath.Join(subTmpDir, "link"), 62 | )) 63 | 64 | excludes := []*regexp.Regexp{ 65 | regexp.MustCompile(".md"), 66 | regexp.MustCompile("5"), 67 | } 68 | 69 | tarFilePath := filepath.Join(baseTmpDir, "res.tar.gz") 70 | require.NoError(t, Compress(tarFilePath, baseTmpDir, excludes...)) 71 | require.FileExists(t, tarFilePath) 72 | 73 | res := []string{"1.txt", "2.bin", "sub/4.txt", "sub/link"} 74 | 75 | require.NoError(t, iterateTarball( 76 | tarFilePath, func(_ *tar.Reader, header *tar.Header) (bool, error) { 77 | require.Equal(t, res[0], header.Name) 78 | res = res[1:] 79 | 80 | return false, nil 81 | }), 82 | ) 83 | } 84 | 85 | func TestCompressWithoutPreservingPath(t *testing.T) { 86 | baseTmpDir := t.TempDir() 87 | compressDir := filepath.Join(baseTmpDir, "to_compress") 88 | require.NoError(t, os.MkdirAll(compressDir, os.FileMode(0o755))) 89 | 90 | for _, fileName := range []string{ 91 | "1.txt", "2.bin", "3.md", 92 | } { 93 | require.NoError(t, os.WriteFile( 94 | filepath.Join(compressDir, fileName), 95 | []byte{1, 2, 3}, 96 | os.FileMode(0o644), 97 | )) 98 | } 99 | 100 | logrus.SetLevel(logrus.DebugLevel) 101 | 102 | tarFilePath := filepath.Join(baseTmpDir, "res.tar.gz") 103 | require.NoError(t, CompressWithoutPreservingPath(tarFilePath, compressDir)) 104 | require.FileExists(t, tarFilePath) 105 | 106 | res := []string{"1.txt", "2.bin", "3.md"} 107 | 108 | require.NoError(t, iterateTarball( 109 | tarFilePath, func(_ *tar.Reader, header *tar.Header) (bool, error) { 110 | require.Equal(t, res[0], header.Name) 111 | res = res[1:] 112 | 113 | return false, nil 114 | }), 115 | ) 116 | } 117 | 118 | func TestExtract(t *testing.T) { 119 | tarball := []byte{ 120 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xec, 0xd7, 121 | 0xdf, 0xea, 0x82, 0x30, 0x14, 0xc0, 0xf1, 0xfd, 0xfe, 0xd4, 0x73, 0xf4, 122 | 0x02, 0xb9, 0xb3, 0x9d, 0xb9, 0xd9, 0xe3, 0xa8, 0x08, 0x49, 0x69, 0xe2, 123 | 0x26, 0xf4, 0xf8, 0x31, 0x93, 0x2e, 0xba, 0x10, 0x8a, 0xe6, 0x08, 0xcf, 124 | 0xe7, 0x66, 0x0c, 0xc4, 0x1d, 0x2f, 0xbe, 0x30, 0x45, 0xe2, 0xae, 0x8e, 125 | 0x85, 0x05, 0x00, 0xa0, 0x95, 0xf2, 0xab, 0x30, 0x29, 0x8c, 0x7b, 0x71, 126 | 0xdf, 0x4f, 0x90, 0x09, 0x34, 0x29, 0x00, 0x48, 0xd4, 0x9a, 0x81, 0x90, 127 | 0x0a, 0x91, 0xed, 0x20, 0xf0, 0x5c, 0xa3, 0xc1, 0xba, 0xbc, 0x67, 0x00, 128 | 0x36, 0xb7, 0xe5, 0x31, 0x9f, 0x7b, 0xae, 0xea, 0xed, 0xcc, 0x7b, 0xa6, 129 | 0x2f, 0x79, 0xac, 0x5f, 0xe2, 0xe7, 0xf7, 0x2f, 0xf6, 0x08, 0x24, 0x22, 130 | 0x99, 0x14, 0x75, 0x1b, 0xf8, 0x8c, 0x37, 0xfa, 0x47, 0x9d, 0x52, 0xff, 131 | 0x4b, 0xa0, 0xfe, 0xd7, 0xcd, 0x0e, 0x05, 0x57, 0x81, 0xef, 0x00, 0xaf, 132 | 0xf7, 0x8f, 0x52, 0x1a, 0xea, 0x7f, 0x09, 0xff, 0x9b, 0x6d, 0xec, 0x11, 133 | 0x48, 0x44, 0xbe, 0xff, 0x73, 0xdd, 0x9e, 0x42, 0x9e, 0xe1, 0x7b, 0x30, 134 | 0xc6, 0xcc, 0xf4, 0x0f, 0x4f, 0xfd, 0x1b, 0xed, 0xef, 0xff, 0x92, 0xbb, 135 | 0xa6, 0xe3, 0xe5, 0xa5, 0xe9, 0xfa, 0xca, 0xda, 0xfd, 0x01, 0x33, 0x25, 136 | 0x54, 0x86, 0x8a, 0x7f, 0xf2, 0xa7, 0x65, 0xe5, 0xfd, 0x13, 0x42, 0xd6, 137 | 0xeb, 0x16, 0x00, 0x00, 0xff, 0xff, 0xe9, 0xde, 0xbe, 0xdf, 0x00, 0x12, 138 | 0x00, 0x00, 139 | } 140 | file, err := os.CreateTemp(t.TempDir(), "tarball") 141 | require.NoError(t, err) 142 | 143 | defer os.Remove(file.Name()) 144 | 145 | _, err = file.Write(tarball) 146 | require.NoError(t, err) 147 | 148 | baseTmpDir := t.TempDir() 149 | require.NoError(t, Extract(file.Name(), baseTmpDir)) 150 | res := []string{ 151 | filepath.Base(baseTmpDir), 152 | "1.txt", 153 | "2.bin", 154 | "sub", 155 | "4.txt", 156 | "link", 157 | } 158 | 159 | require.NoError(t, filepath.Walk( 160 | baseTmpDir, 161 | func(_ string, fileInfo os.FileInfo, _ error) error { 162 | require.Equal(t, res[0], fileInfo.Name()) 163 | 164 | if res[0] == "link" { 165 | require.Equal(t, os.ModeSymlink, fileInfo.Mode()&os.ModeSymlink) 166 | } 167 | 168 | res = res[1:] 169 | 170 | return nil 171 | }, 172 | )) 173 | } 174 | 175 | func TestReadFileFromGzippedTar(t *testing.T) { 176 | baseTmpDir := t.TempDir() 177 | 178 | const ( 179 | testFilePath = "test.txt" 180 | testFileContents = "test-file-contents" 181 | ) 182 | 183 | testTarPath := filepath.Join(baseTmpDir, "test.tar.gz") 184 | 185 | require.NoError(t, os.WriteFile( 186 | filepath.Join(baseTmpDir, testFilePath), 187 | []byte(testFileContents), 188 | os.FileMode(0o644), 189 | )) 190 | require.NoError(t, Compress(testTarPath, baseTmpDir, nil)) 191 | 192 | type args struct { 193 | tarPath string 194 | filePath string 195 | } 196 | 197 | type want struct { 198 | fileContents string 199 | shouldErr bool 200 | } 201 | 202 | cases := map[string]struct { 203 | args args 204 | want want 205 | }{ 206 | "FoundFileInTar": { 207 | args: args{ 208 | tarPath: testTarPath, 209 | filePath: testFilePath, 210 | }, 211 | want: want{fileContents: testFileContents}, 212 | }, 213 | "FileNotInTar": { 214 | args: args{ 215 | tarPath: testTarPath, 216 | filePath: "badfile.txt", 217 | }, 218 | want: want{shouldErr: true}, 219 | }, 220 | } 221 | 222 | for name, tc := range cases { 223 | t.Run(name, func(t *testing.T) { 224 | r, err := ReadFileFromGzippedTar(tc.args.tarPath, tc.args.filePath) 225 | if tc.want.shouldErr { 226 | require.Nil(t, r) 227 | require.Error(t, err) 228 | } else { 229 | file, err := io.ReadAll(r) 230 | require.NoError(t, err) 231 | require.Equal(t, tc.want.fileContents, string(file)) 232 | } 233 | }) 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /tar/tar.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package tar 18 | 19 | import ( 20 | "archive/tar" 21 | "compress/gzip" 22 | "errors" 23 | "fmt" 24 | "io" 25 | "os" 26 | "path/filepath" 27 | "regexp" 28 | "strings" 29 | 30 | "github.com/sirupsen/logrus" 31 | ) 32 | 33 | // Compress the provided `tarContentsPath` into the `tarFilePath` while 34 | // excluding the `exclude` regular expression patterns. This function will 35 | // preserve path between `tarFilePath` and `tarContentsPath` directories inside 36 | // the archive (see `CompressWithoutPreservingPath` as an alternative). 37 | func Compress(tarFilePath, tarContentsPath string, excludes ...*regexp.Regexp) error { 38 | return compress(true, tarFilePath, tarContentsPath, excludes...) 39 | } 40 | 41 | // Compress the provided `tarContentsPath` into the `tarFilePath` while 42 | // excluding the `exclude` regular expression patterns. This function will 43 | // not preserve path leading to the `tarContentsPath` directory in the archive. 44 | func CompressWithoutPreservingPath(tarFilePath, tarContentsPath string, excludes ...*regexp.Regexp) error { 45 | return compress(false, tarFilePath, tarContentsPath, excludes...) 46 | } 47 | 48 | func compress(preserveRootDirStructure bool, tarFilePath, tarContentsPath string, excludes ...*regexp.Regexp) error { 49 | tarFile, err := os.Create(tarFilePath) 50 | if err != nil { 51 | return fmt.Errorf("create tar file %q: %w", tarFilePath, err) 52 | } 53 | defer tarFile.Close() 54 | 55 | gzipWriter := gzip.NewWriter(tarFile) 56 | defer gzipWriter.Close() 57 | 58 | tarWriter := tar.NewWriter(gzipWriter) 59 | defer tarWriter.Close() 60 | 61 | if err := filepath.Walk(tarContentsPath, func(filePath string, fileInfo os.FileInfo, err error) error { 62 | if err != nil { 63 | return err 64 | } 65 | 66 | var link string 67 | isLink := fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink 68 | if isLink { 69 | link, err = os.Readlink(filePath) 70 | if err != nil { 71 | return fmt.Errorf("read file link of %s: %w", filePath, err) 72 | } 73 | } 74 | 75 | header, err := tar.FileInfoHeader(fileInfo, link) 76 | if err != nil { 77 | return fmt.Errorf("create file info header for %q: %w", filePath, err) 78 | } 79 | 80 | if fileInfo.IsDir() || filePath == tarFilePath { 81 | logrus.Tracef("Skipping: %s", filePath) 82 | 83 | return nil 84 | } 85 | 86 | for _, re := range excludes { 87 | if re != nil && re.MatchString(filePath) { 88 | logrus.Tracef("Excluding: %s", filePath) 89 | 90 | return nil 91 | } 92 | } 93 | 94 | // Make the path inside the tar relative to the archive path if 95 | // necessary. 96 | // 97 | // The default way this works is that we preserve the path between 98 | // `tarFilePath` and `tarContentsPath` directories inside the archive. 99 | // This might not work well if `tarFilePath` and `tarContentsPath` 100 | // are on different levels in the file system (e.g. they don't have 101 | // common parent directory). 102 | // In such case we can disable `preserveRootDirStructure` flag which 103 | // will make paths inside the archive relative to `tarContentsPath`. 104 | dropPath := filepath.Dir(tarFilePath) 105 | if !preserveRootDirStructure { 106 | dropPath = tarContentsPath 107 | } 108 | header.Name = strings.TrimLeft( 109 | strings.TrimPrefix(filePath, dropPath), 110 | string(filepath.Separator), 111 | ) 112 | header.Linkname = filepath.ToSlash(header.Linkname) 113 | 114 | if err := tarWriter.WriteHeader(header); err != nil { 115 | return fmt.Errorf("writing tar header: %w", err) 116 | } 117 | 118 | if !isLink { 119 | file, err := os.Open(filePath) 120 | if err != nil { 121 | return fmt.Errorf("open file %q: %w", filePath, err) 122 | } 123 | 124 | if _, err := io.Copy(tarWriter, file); err != nil { 125 | return fmt.Errorf("writing file to tar writer: %w", err) 126 | } 127 | 128 | file.Close() 129 | } 130 | 131 | return nil 132 | }); err != nil { 133 | return fmt.Errorf("walking tree in %q: %w", tarContentsPath, err) 134 | } 135 | 136 | return nil 137 | } 138 | 139 | // Extract can be used to extract the provided `tarFilePath` into the 140 | // `destinationPath`. 141 | func Extract(tarFilePath, destinationPath string) error { 142 | return iterateTarball( 143 | tarFilePath, 144 | func(reader *tar.Reader, header *tar.Header) (stop bool, err error) { 145 | switch header.Typeflag { 146 | case tar.TypeDir: 147 | targetDir, err := SanitizeArchivePath(destinationPath, header.Name) 148 | if err != nil { 149 | return false, fmt.Errorf("SanitizeArchivePath: %w", err) 150 | } 151 | 152 | logrus.Tracef("Creating directory %s", targetDir) 153 | 154 | if err := os.MkdirAll(targetDir, os.FileMode(0o755)); err != nil { 155 | return false, fmt.Errorf("create target directory: %w", err) 156 | } 157 | case tar.TypeSymlink: 158 | targetFile, err := SanitizeArchivePath(destinationPath, header.Name) 159 | if err != nil { 160 | return false, fmt.Errorf("SanitizeArchivePath: %w", err) 161 | } 162 | 163 | logrus.Tracef( 164 | "Creating symlink %s -> %s", header.Linkname, targetFile, 165 | ) 166 | 167 | if err := os.MkdirAll( 168 | filepath.Dir(targetFile), os.FileMode(0o755), 169 | ); err != nil { 170 | return false, fmt.Errorf("create target directory: %w", err) 171 | } 172 | 173 | if err := os.Symlink(header.Linkname, targetFile); err != nil { 174 | return false, fmt.Errorf("create symlink: %w", err) 175 | } 176 | // tar.TypeRegA has been deprecated since Go 1.11 177 | // should we just remove? 178 | case tar.TypeReg: 179 | targetFile, err := SanitizeArchivePath(destinationPath, header.Name) 180 | if err != nil { 181 | return false, fmt.Errorf("SanitizeArchivePath: %w", err) 182 | } 183 | 184 | logrus.Tracef("Creating file %s", targetFile) 185 | 186 | if err := os.MkdirAll( 187 | filepath.Dir(targetFile), os.FileMode(0o755), 188 | ); err != nil { 189 | return false, fmt.Errorf("create target directory: %w", err) 190 | } 191 | 192 | outFile, err := os.Create(targetFile) 193 | if err != nil { 194 | return false, fmt.Errorf("create target file: %w", err) 195 | } 196 | //nolint:gosec // integer overflow highly unlikely 197 | if err := outFile.Chmod(os.FileMode(header.Mode)); err != nil { 198 | return false, fmt.Errorf("chmod target file: %w", err) 199 | } 200 | 201 | if _, err := io.Copy(outFile, reader); err != nil { 202 | return false, fmt.Errorf("copy file contents %s: %w", targetFile, err) 203 | } 204 | 205 | outFile.Close() 206 | 207 | default: 208 | logrus.Warnf( 209 | "File %s has unknown type %s", 210 | header.Name, string(header.Typeflag), 211 | ) 212 | } 213 | 214 | return false, nil 215 | }, 216 | ) 217 | } 218 | 219 | // Sanitize archive file pathing from "G305: Zip Slip vulnerability" 220 | // https://security.snyk.io/research/zip-slip-vulnerability 221 | func SanitizeArchivePath(d, t string) (v string, err error) { 222 | v = filepath.Join(d, t) 223 | if strings.HasPrefix(v, filepath.Clean(d)) { 224 | return v, nil 225 | } 226 | 227 | return "", fmt.Errorf("%s: %s", "content filepath is tainted", t) 228 | } 229 | 230 | // ReadFileFromGzippedTar opens a tarball and reads contents of a file inside. 231 | func ReadFileFromGzippedTar( 232 | tarPath, filePath string, 233 | ) (res io.Reader, err error) { 234 | if err := iterateTarball( 235 | tarPath, 236 | func(reader *tar.Reader, header *tar.Header) (stop bool, err error) { 237 | if header.Name == filePath { 238 | res = reader 239 | 240 | return true, nil 241 | } 242 | 243 | return false, nil 244 | }, 245 | ); err != nil { 246 | return nil, err 247 | } 248 | 249 | if res == nil { 250 | return nil, fmt.Errorf("unable to find file %q in tarball %q: %w", tarPath, filePath, err) 251 | } 252 | 253 | return res, nil 254 | } 255 | 256 | // iterateTarball can be used to iterate over the contents of a tarball by 257 | // calling the callback for each entry. 258 | func iterateTarball( 259 | tarPath string, 260 | callback func(*tar.Reader, *tar.Header) (stop bool, err error), 261 | ) error { 262 | file, err := os.Open(tarPath) 263 | if err != nil { 264 | return fmt.Errorf("opening tar file %q: %w", tarPath, err) 265 | } 266 | 267 | gzipReader, err := gzip.NewReader(file) 268 | if err != nil { 269 | return fmt.Errorf("creating gzip reader for file %q: %w", tarPath, err) 270 | } 271 | 272 | tarReader := tar.NewReader(gzipReader) 273 | 274 | for { 275 | tarHeader, err := tarReader.Next() 276 | if errors.Is(err, io.EOF) { 277 | break // End of archive 278 | } 279 | 280 | stop, err := callback(tarReader, tarHeader) 281 | if err != nil { 282 | return err 283 | } 284 | 285 | if stop { 286 | // User wants to stop 287 | break 288 | } 289 | } 290 | 291 | return nil 292 | } 293 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= 2 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 3 | github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= 4 | github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 5 | github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio= 6 | github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q= 7 | github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 8 | github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 9 | github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s= 10 | github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= 11 | github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= 12 | github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= 13 | github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= 14 | github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= 15 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= 16 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= 17 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 18 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 19 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 20 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 21 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 23 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 25 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 26 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 27 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 28 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 29 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 30 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 31 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 32 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 33 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 34 | github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= 35 | github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= 36 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 37 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 38 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 39 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 40 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 41 | github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= 42 | github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= 43 | github.com/maxbrunsfeld/counterfeiter/v6 v6.12.1 h1:D4O2wLxB384TS3ohBJMfolnxb4qGmoZ1PnWNtit8LYo= 44 | github.com/maxbrunsfeld/counterfeiter/v6 v6.12.1/go.mod h1:RuJdxo0oI6dClIaMzdl3hewq3a065RH65dofJP03h8I= 45 | github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= 46 | github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= 47 | github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 h1:Up6+btDp321ZG5/zdSLo48H9Iaq0UQGthrhWC6pCxzE= 48 | github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481/go.mod h1:yKZQO8QE2bHlgozqWDiRVqTFlLQSj30K/6SAK8EeYFw= 49 | github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= 50 | github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= 51 | github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= 52 | github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= 53 | github.com/olekukonko/ll v0.1.3 h1:sV2jrhQGq5B3W0nENUISCR6azIPf7UBUpVq0x/y70Fg= 54 | github.com/olekukonko/ll v0.1.3/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew= 55 | github.com/olekukonko/tablewriter v1.1.2 h1:L2kI1Y5tZBct/O/TyZK1zIE9GlBj/TVs+AY5tZDCDSc= 56 | github.com/olekukonko/tablewriter v1.1.2/go.mod h1:z7SYPugVqGVavWoA2sGsFIoOVNmEHxUAAMrhXONtfkg= 57 | github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= 58 | github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= 59 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 60 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 61 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 62 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 63 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 64 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 65 | github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= 66 | github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= 67 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 68 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 69 | github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= 70 | github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= 71 | github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= 72 | github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 73 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 74 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 75 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 76 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 77 | github.com/uwu-tools/magex v0.10.1 h1:qEJtkM+5nGKt/3BaRgj+X7pf+pNZ4SDyEEPMzEeUjkw= 78 | github.com/uwu-tools/magex v0.10.1/go.mod h1:5uQvmocqEueCbgK4Dm67mIfhjq80o408F17J6867go8= 79 | go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= 80 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 81 | golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= 82 | golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= 83 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 84 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 85 | golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= 86 | golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 87 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 88 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 89 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 90 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 91 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 92 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 93 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 94 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 95 | golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= 96 | golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= 97 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 98 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 99 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 100 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 101 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 102 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 103 | k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0= 104 | k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 105 | -------------------------------------------------------------------------------- /http/httpfakes/fake_agent_implementation.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by counterfeiter. DO NOT EDIT. 18 | package httpfakes 19 | 20 | import ( 21 | httpa "net/http" 22 | "sync" 23 | 24 | "sigs.k8s.io/release-utils/http" 25 | ) 26 | 27 | type FakeAgentImplementation struct { 28 | SendGetRequestStub func(*httpa.Client, string) (*httpa.Response, error) 29 | sendGetRequestMutex sync.RWMutex 30 | sendGetRequestArgsForCall []struct { 31 | arg1 *httpa.Client 32 | arg2 string 33 | } 34 | sendGetRequestReturns struct { 35 | result1 *httpa.Response 36 | result2 error 37 | } 38 | sendGetRequestReturnsOnCall map[int]struct { 39 | result1 *httpa.Response 40 | result2 error 41 | } 42 | SendHeadRequestStub func(*httpa.Client, string) (*httpa.Response, error) 43 | sendHeadRequestMutex sync.RWMutex 44 | sendHeadRequestArgsForCall []struct { 45 | arg1 *httpa.Client 46 | arg2 string 47 | } 48 | sendHeadRequestReturns struct { 49 | result1 *httpa.Response 50 | result2 error 51 | } 52 | sendHeadRequestReturnsOnCall map[int]struct { 53 | result1 *httpa.Response 54 | result2 error 55 | } 56 | SendPostRequestStub func(*httpa.Client, string, []byte, string) (*httpa.Response, error) 57 | sendPostRequestMutex sync.RWMutex 58 | sendPostRequestArgsForCall []struct { 59 | arg1 *httpa.Client 60 | arg2 string 61 | arg3 []byte 62 | arg4 string 63 | } 64 | sendPostRequestReturns struct { 65 | result1 *httpa.Response 66 | result2 error 67 | } 68 | sendPostRequestReturnsOnCall map[int]struct { 69 | result1 *httpa.Response 70 | result2 error 71 | } 72 | invocations map[string][][]interface{} 73 | invocationsMutex sync.RWMutex 74 | } 75 | 76 | func (fake *FakeAgentImplementation) SendGetRequest(arg1 *httpa.Client, arg2 string) (*httpa.Response, error) { 77 | fake.sendGetRequestMutex.Lock() 78 | ret, specificReturn := fake.sendGetRequestReturnsOnCall[len(fake.sendGetRequestArgsForCall)] 79 | fake.sendGetRequestArgsForCall = append(fake.sendGetRequestArgsForCall, struct { 80 | arg1 *httpa.Client 81 | arg2 string 82 | }{arg1, arg2}) 83 | stub := fake.SendGetRequestStub 84 | fakeReturns := fake.sendGetRequestReturns 85 | fake.recordInvocation("SendGetRequest", []interface{}{arg1, arg2}) 86 | fake.sendGetRequestMutex.Unlock() 87 | if stub != nil { 88 | return stub(arg1, arg2) 89 | } 90 | if specificReturn { 91 | return ret.result1, ret.result2 92 | } 93 | return fakeReturns.result1, fakeReturns.result2 94 | } 95 | 96 | func (fake *FakeAgentImplementation) SendGetRequestCallCount() int { 97 | fake.sendGetRequestMutex.RLock() 98 | defer fake.sendGetRequestMutex.RUnlock() 99 | return len(fake.sendGetRequestArgsForCall) 100 | } 101 | 102 | func (fake *FakeAgentImplementation) SendGetRequestCalls(stub func(*httpa.Client, string) (*httpa.Response, error)) { 103 | fake.sendGetRequestMutex.Lock() 104 | defer fake.sendGetRequestMutex.Unlock() 105 | fake.SendGetRequestStub = stub 106 | } 107 | 108 | func (fake *FakeAgentImplementation) SendGetRequestArgsForCall(i int) (*httpa.Client, string) { 109 | fake.sendGetRequestMutex.RLock() 110 | defer fake.sendGetRequestMutex.RUnlock() 111 | argsForCall := fake.sendGetRequestArgsForCall[i] 112 | return argsForCall.arg1, argsForCall.arg2 113 | } 114 | 115 | func (fake *FakeAgentImplementation) SendGetRequestReturns(result1 *httpa.Response, result2 error) { 116 | fake.sendGetRequestMutex.Lock() 117 | defer fake.sendGetRequestMutex.Unlock() 118 | fake.SendGetRequestStub = nil 119 | fake.sendGetRequestReturns = struct { 120 | result1 *httpa.Response 121 | result2 error 122 | }{result1, result2} 123 | } 124 | 125 | func (fake *FakeAgentImplementation) SendGetRequestReturnsOnCall(i int, result1 *httpa.Response, result2 error) { 126 | fake.sendGetRequestMutex.Lock() 127 | defer fake.sendGetRequestMutex.Unlock() 128 | fake.SendGetRequestStub = nil 129 | if fake.sendGetRequestReturnsOnCall == nil { 130 | fake.sendGetRequestReturnsOnCall = make(map[int]struct { 131 | result1 *httpa.Response 132 | result2 error 133 | }) 134 | } 135 | fake.sendGetRequestReturnsOnCall[i] = struct { 136 | result1 *httpa.Response 137 | result2 error 138 | }{result1, result2} 139 | } 140 | 141 | func (fake *FakeAgentImplementation) SendHeadRequest(arg1 *httpa.Client, arg2 string) (*httpa.Response, error) { 142 | fake.sendHeadRequestMutex.Lock() 143 | ret, specificReturn := fake.sendHeadRequestReturnsOnCall[len(fake.sendHeadRequestArgsForCall)] 144 | fake.sendHeadRequestArgsForCall = append(fake.sendHeadRequestArgsForCall, struct { 145 | arg1 *httpa.Client 146 | arg2 string 147 | }{arg1, arg2}) 148 | stub := fake.SendHeadRequestStub 149 | fakeReturns := fake.sendHeadRequestReturns 150 | fake.recordInvocation("SendHeadRequest", []interface{}{arg1, arg2}) 151 | fake.sendHeadRequestMutex.Unlock() 152 | if stub != nil { 153 | return stub(arg1, arg2) 154 | } 155 | if specificReturn { 156 | return ret.result1, ret.result2 157 | } 158 | return fakeReturns.result1, fakeReturns.result2 159 | } 160 | 161 | func (fake *FakeAgentImplementation) SendHeadRequestCallCount() int { 162 | fake.sendHeadRequestMutex.RLock() 163 | defer fake.sendHeadRequestMutex.RUnlock() 164 | return len(fake.sendHeadRequestArgsForCall) 165 | } 166 | 167 | func (fake *FakeAgentImplementation) SendHeadRequestCalls(stub func(*httpa.Client, string) (*httpa.Response, error)) { 168 | fake.sendHeadRequestMutex.Lock() 169 | defer fake.sendHeadRequestMutex.Unlock() 170 | fake.SendHeadRequestStub = stub 171 | } 172 | 173 | func (fake *FakeAgentImplementation) SendHeadRequestArgsForCall(i int) (*httpa.Client, string) { 174 | fake.sendHeadRequestMutex.RLock() 175 | defer fake.sendHeadRequestMutex.RUnlock() 176 | argsForCall := fake.sendHeadRequestArgsForCall[i] 177 | return argsForCall.arg1, argsForCall.arg2 178 | } 179 | 180 | func (fake *FakeAgentImplementation) SendHeadRequestReturns(result1 *httpa.Response, result2 error) { 181 | fake.sendHeadRequestMutex.Lock() 182 | defer fake.sendHeadRequestMutex.Unlock() 183 | fake.SendHeadRequestStub = nil 184 | fake.sendHeadRequestReturns = struct { 185 | result1 *httpa.Response 186 | result2 error 187 | }{result1, result2} 188 | } 189 | 190 | func (fake *FakeAgentImplementation) SendHeadRequestReturnsOnCall(i int, result1 *httpa.Response, result2 error) { 191 | fake.sendHeadRequestMutex.Lock() 192 | defer fake.sendHeadRequestMutex.Unlock() 193 | fake.SendHeadRequestStub = nil 194 | if fake.sendHeadRequestReturnsOnCall == nil { 195 | fake.sendHeadRequestReturnsOnCall = make(map[int]struct { 196 | result1 *httpa.Response 197 | result2 error 198 | }) 199 | } 200 | fake.sendHeadRequestReturnsOnCall[i] = struct { 201 | result1 *httpa.Response 202 | result2 error 203 | }{result1, result2} 204 | } 205 | 206 | func (fake *FakeAgentImplementation) SendPostRequest(arg1 *httpa.Client, arg2 string, arg3 []byte, arg4 string) (*httpa.Response, error) { 207 | var arg3Copy []byte 208 | if arg3 != nil { 209 | arg3Copy = make([]byte, len(arg3)) 210 | copy(arg3Copy, arg3) 211 | } 212 | fake.sendPostRequestMutex.Lock() 213 | ret, specificReturn := fake.sendPostRequestReturnsOnCall[len(fake.sendPostRequestArgsForCall)] 214 | fake.sendPostRequestArgsForCall = append(fake.sendPostRequestArgsForCall, struct { 215 | arg1 *httpa.Client 216 | arg2 string 217 | arg3 []byte 218 | arg4 string 219 | }{arg1, arg2, arg3Copy, arg4}) 220 | stub := fake.SendPostRequestStub 221 | fakeReturns := fake.sendPostRequestReturns 222 | fake.recordInvocation("SendPostRequest", []interface{}{arg1, arg2, arg3Copy, arg4}) 223 | fake.sendPostRequestMutex.Unlock() 224 | if stub != nil { 225 | return stub(arg1, arg2, arg3, arg4) 226 | } 227 | if specificReturn { 228 | return ret.result1, ret.result2 229 | } 230 | return fakeReturns.result1, fakeReturns.result2 231 | } 232 | 233 | func (fake *FakeAgentImplementation) SendPostRequestCallCount() int { 234 | fake.sendPostRequestMutex.RLock() 235 | defer fake.sendPostRequestMutex.RUnlock() 236 | return len(fake.sendPostRequestArgsForCall) 237 | } 238 | 239 | func (fake *FakeAgentImplementation) SendPostRequestCalls(stub func(*httpa.Client, string, []byte, string) (*httpa.Response, error)) { 240 | fake.sendPostRequestMutex.Lock() 241 | defer fake.sendPostRequestMutex.Unlock() 242 | fake.SendPostRequestStub = stub 243 | } 244 | 245 | func (fake *FakeAgentImplementation) SendPostRequestArgsForCall(i int) (*httpa.Client, string, []byte, string) { 246 | fake.sendPostRequestMutex.RLock() 247 | defer fake.sendPostRequestMutex.RUnlock() 248 | argsForCall := fake.sendPostRequestArgsForCall[i] 249 | return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 250 | } 251 | 252 | func (fake *FakeAgentImplementation) SendPostRequestReturns(result1 *httpa.Response, result2 error) { 253 | fake.sendPostRequestMutex.Lock() 254 | defer fake.sendPostRequestMutex.Unlock() 255 | fake.SendPostRequestStub = nil 256 | fake.sendPostRequestReturns = struct { 257 | result1 *httpa.Response 258 | result2 error 259 | }{result1, result2} 260 | } 261 | 262 | func (fake *FakeAgentImplementation) SendPostRequestReturnsOnCall(i int, result1 *httpa.Response, result2 error) { 263 | fake.sendPostRequestMutex.Lock() 264 | defer fake.sendPostRequestMutex.Unlock() 265 | fake.SendPostRequestStub = nil 266 | if fake.sendPostRequestReturnsOnCall == nil { 267 | fake.sendPostRequestReturnsOnCall = make(map[int]struct { 268 | result1 *httpa.Response 269 | result2 error 270 | }) 271 | } 272 | fake.sendPostRequestReturnsOnCall[i] = struct { 273 | result1 *httpa.Response 274 | result2 error 275 | }{result1, result2} 276 | } 277 | 278 | func (fake *FakeAgentImplementation) Invocations() map[string][][]interface{} { 279 | fake.invocationsMutex.RLock() 280 | defer fake.invocationsMutex.RUnlock() 281 | fake.sendGetRequestMutex.RLock() 282 | defer fake.sendGetRequestMutex.RUnlock() 283 | fake.sendHeadRequestMutex.RLock() 284 | defer fake.sendHeadRequestMutex.RUnlock() 285 | fake.sendPostRequestMutex.RLock() 286 | defer fake.sendPostRequestMutex.RUnlock() 287 | copiedInvocations := map[string][][]interface{}{} 288 | for key, value := range fake.invocations { 289 | copiedInvocations[key] = value 290 | } 291 | return copiedInvocations 292 | } 293 | 294 | func (fake *FakeAgentImplementation) recordInvocation(key string, args []interface{}) { 295 | fake.invocationsMutex.Lock() 296 | defer fake.invocationsMutex.Unlock() 297 | if fake.invocations == nil { 298 | fake.invocations = map[string][][]interface{}{} 299 | } 300 | if fake.invocations[key] == nil { 301 | fake.invocations[key] = [][]interface{}{} 302 | } 303 | fake.invocations[key] = append(fake.invocations[key], args) 304 | } 305 | 306 | var _ http.AgentImplementation = new(FakeAgentImplementation) 307 | -------------------------------------------------------------------------------- /command/command_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package command 18 | 19 | import ( 20 | "bytes" 21 | "os" 22 | "testing" 23 | 24 | "github.com/stretchr/testify/require" 25 | ) 26 | 27 | func TestSuccess(t *testing.T) { 28 | res, err := New("echo", "hi").Run() 29 | require.NoError(t, err) 30 | require.True(t, res.Success()) 31 | require.Zero(t, res.ExitCode()) 32 | } 33 | 34 | func TestSuccessPipe(t *testing.T) { 35 | res, err := New("echo", "-n", "hi"). 36 | Pipe("cat"). 37 | Pipe("cat"). 38 | Run() 39 | require.NoError(t, err) 40 | require.True(t, res.Success()) 41 | require.Zero(t, res.ExitCode()) 42 | require.Equal(t, "hi", res.Output()) 43 | } 44 | 45 | func TestFailurePipeWrongCommand(t *testing.T) { 46 | res, err := New("echo", "-n", "hi"). 47 | Pipe("wrong"). 48 | Run() 49 | require.Error(t, err) 50 | require.Nil(t, res) 51 | } 52 | 53 | func TestFailurePipeWrongArgument(t *testing.T) { 54 | res, err := New("echo", "-n", "hi"). 55 | Pipe("cat", "--wrong"). 56 | Run() 57 | require.NoError(t, err) 58 | require.False(t, res.Success()) 59 | require.Empty(t, res.Output()) 60 | require.NotEmpty(t, res.Error()) 61 | } 62 | 63 | func TestSuccessVerbose(t *testing.T) { 64 | res, err := New("echo", "hi").Verbose().Run() 65 | require.NoError(t, err) 66 | require.True(t, res.Success()) 67 | require.Zero(t, res.ExitCode()) 68 | } 69 | 70 | func TestSuccessWithWorkingDir(t *testing.T) { 71 | res, err := NewWithWorkDir("/", "ls", "-1").Run() 72 | require.NoError(t, err) 73 | require.True(t, res.Success()) 74 | require.Zero(t, res.ExitCode()) 75 | } 76 | 77 | func TestFailureWithWrongWorkingDir(t *testing.T) { 78 | res, err := NewWithWorkDir("/should/not/exist", "ls", "-1").Run() 79 | require.Error(t, err) 80 | require.Nil(t, res) 81 | } 82 | 83 | func TestSuccessSilent(t *testing.T) { 84 | res, err := New("echo", "hi").RunSilent() 85 | require.NoError(t, err) 86 | require.True(t, res.Success()) 87 | } 88 | 89 | func TestSuccessSeparated(t *testing.T) { 90 | res, err := New("echo", "hi").RunSilent() 91 | require.NoError(t, err) 92 | require.True(t, res.Success()) 93 | } 94 | 95 | func TestSuccessSingleArgument(t *testing.T) { 96 | res, err := New("echo").Run() 97 | require.NoError(t, err) 98 | require.True(t, res.Success()) 99 | } 100 | 101 | func TestSuccessNoArgument(t *testing.T) { 102 | res, err := New("").Run() 103 | require.Error(t, err) 104 | require.Nil(t, res) 105 | } 106 | 107 | func TestSuccessOutput(t *testing.T) { 108 | res, err := New("echo", "-n", "hello world").Run() 109 | require.NoError(t, err) 110 | require.Equal(t, "hello world", res.Output()) 111 | } 112 | 113 | func TestSuccessOutputTrimNL(t *testing.T) { 114 | res, err := New("echo", "-n", "hello world\n").Run() 115 | require.NoError(t, err) 116 | require.Equal(t, "hello world", res.OutputTrimNL()) 117 | } 118 | 119 | func TestSuccessError(t *testing.T) { 120 | res, err := New("cat", "/not/valid").Run() 121 | require.NoError(t, err) 122 | require.Empty(t, res.Output()) 123 | require.Contains(t, res.Error(), "No such file") 124 | } 125 | 126 | func TestSuccessOutputSeparated(t *testing.T) { 127 | res, err := New("echo", "-n", "hello").Run() 128 | require.NoError(t, err) 129 | require.Equal(t, "hello", res.Output()) 130 | } 131 | 132 | func TestFailureStdErr(t *testing.T) { 133 | res, err := New("cat", "/not/valid").Run() 134 | require.NoError(t, err) 135 | require.False(t, res.Success()) 136 | require.Equal(t, 1, res.ExitCode()) 137 | } 138 | 139 | func TestFailureNotExisting(t *testing.T) { 140 | res, err := New("/not/valid").Run() 141 | require.Error(t, err) 142 | require.Nil(t, res) 143 | } 144 | 145 | func TestSuccessExecute(t *testing.T) { 146 | err := Execute("echo", "-n", "hi", "ho") 147 | require.NoError(t, err) 148 | } 149 | 150 | func TestFailureExecute(t *testing.T) { 151 | err := Execute("cat", "/not/invalid") 152 | require.Error(t, err) 153 | } 154 | 155 | func TestAvailableSuccessValidCommand(t *testing.T) { 156 | res := Available("echo") 157 | require.True(t, res) 158 | } 159 | 160 | func TestAvailableSuccessEmptyCommands(t *testing.T) { 161 | res := Available() 162 | require.True(t, res) 163 | } 164 | 165 | func TestAvailableFailure(t *testing.T) { 166 | res := Available("echo", "this-command-should-not-exist") 167 | require.False(t, res) 168 | } 169 | 170 | func TestSuccessRunSuccess(t *testing.T) { 171 | require.NoError(t, New("echo", "hi").RunSuccess()) 172 | } 173 | 174 | func TestFailureRunSuccess(t *testing.T) { 175 | require.Error(t, New("cat", "/not/available").RunSuccess()) 176 | } 177 | 178 | func TestSuccessRunSilentSuccess(t *testing.T) { 179 | require.NoError(t, New("echo", "hi").RunSilentSuccess()) 180 | } 181 | 182 | func TestFailureRunSuccessSilent(t *testing.T) { 183 | require.Error(t, New("cat", "/not/available").RunSilentSuccess()) 184 | } 185 | 186 | func TestSuccessRunSuccessOutput(t *testing.T) { 187 | res, err := New("echo", "-n", "hi").RunSuccessOutput() 188 | require.NoError(t, err) 189 | require.Equal(t, "hi", res.Output()) 190 | } 191 | 192 | func TestFailureRunSuccessOutput(t *testing.T) { 193 | res, err := New("cat", "/not/available").RunSuccessOutput() 194 | require.Error(t, err) 195 | require.Nil(t, res) 196 | } 197 | 198 | func TestSuccessRunSilentSuccessOutput(t *testing.T) { 199 | res, err := New("echo", "-n", "hi").RunSilentSuccessOutput() 200 | require.NoError(t, err) 201 | require.Equal(t, "hi", res.Output()) 202 | } 203 | 204 | func TestFailureRunSilentSuccessOutput(t *testing.T) { 205 | res, err := New("cat", "/not/available").RunSilentSuccessOutput() 206 | require.Error(t, err) 207 | require.Nil(t, res) 208 | } 209 | 210 | func TestSuccessLogWriter(t *testing.T) { 211 | f, err := os.CreateTemp(t.TempDir(), "log") 212 | require.NoError(t, err) 213 | 214 | defer func() { require.NoError(t, os.Remove(f.Name())) }() 215 | 216 | res, err := New("echo", "Hello World").AddWriter(f).RunSuccessOutput() 217 | require.NoError(t, err) 218 | 219 | content, err := os.ReadFile(f.Name()) 220 | require.NoError(t, err) 221 | require.Equal(t, res.Output(), string(content)) 222 | } 223 | 224 | func TestSuccessLogWriterMultiple(t *testing.T) { 225 | f, err := os.CreateTemp(t.TempDir(), "log") 226 | require.NoError(t, err) 227 | 228 | defer func() { require.NoError(t, os.Remove(f.Name())) }() 229 | 230 | b := &bytes.Buffer{} 231 | 232 | res, err := New("echo", "Hello World"). 233 | AddWriter(f). 234 | AddWriter(b). 235 | RunSuccessOutput() 236 | require.NoError(t, err) 237 | 238 | content, err := os.ReadFile(f.Name()) 239 | require.NoError(t, err) 240 | require.Equal(t, res.Output(), string(content)) 241 | require.Equal(t, res.Output(), b.String()) 242 | } 243 | 244 | func TestSuccessLogWriterSilent(t *testing.T) { 245 | f, err := os.CreateTemp(t.TempDir(), "log") 246 | require.NoError(t, err) 247 | 248 | defer func() { require.NoError(t, os.Remove(f.Name())) }() 249 | 250 | err = New("echo", "Hello World").AddWriter(f).RunSilentSuccess() 251 | require.NoError(t, err) 252 | 253 | content, err := os.ReadFile(f.Name()) 254 | require.NoError(t, err) 255 | require.Empty(t, content) 256 | } 257 | 258 | func TestSuccessLogWriterStdErr(t *testing.T) { 259 | f, err := os.CreateTemp(t.TempDir(), "log") 260 | require.NoError(t, err) 261 | 262 | defer func() { require.NoError(t, os.Remove(f.Name())) }() 263 | 264 | res, err := New("bash", "-c", ">&2 echo error"). 265 | AddWriter(f).RunSuccessOutput() 266 | require.NoError(t, err) 267 | 268 | content, err := os.ReadFile(f.Name()) 269 | require.NoError(t, err) 270 | require.Equal(t, res.Error(), string(content)) 271 | } 272 | 273 | func TestSuccessLogWriterStdErrAndStdOut(t *testing.T) { 274 | f, err := os.CreateTemp(t.TempDir(), "log") 275 | require.NoError(t, err) 276 | 277 | defer func() { require.NoError(t, os.Remove(f.Name())) }() 278 | 279 | res, err := New("bash", "-c", ">&2 echo stderr; echo stdout"). 280 | AddWriter(f).RunSuccessOutput() 281 | require.NoError(t, err) 282 | 283 | content, err := os.ReadFile(f.Name()) 284 | require.NoError(t, err) 285 | require.Contains(t, string(content), res.Output()) 286 | require.Contains(t, string(content), res.Error()) 287 | } 288 | 289 | func TestSuccessLogWriterStdErrAndStdOutOnlyStdErr(t *testing.T) { 290 | f, err := os.CreateTemp(t.TempDir(), "log") 291 | require.NoError(t, err) 292 | 293 | defer func() { require.NoError(t, os.Remove(f.Name())) }() 294 | 295 | res, err := New("bash", "-c", ">&2 echo stderr; echo stdout"). 296 | AddErrorWriter(f).RunSuccessOutput() 297 | require.NoError(t, err) 298 | 299 | content, err := os.ReadFile(f.Name()) 300 | require.NoError(t, err) 301 | require.Equal(t, res.Error(), string(content)) 302 | } 303 | 304 | func TestSuccessLogWriterStdErrAndStdOutOnlyStdOut(t *testing.T) { 305 | f, err := os.CreateTemp(t.TempDir(), "log") 306 | require.NoError(t, err) 307 | 308 | defer func() { require.NoError(t, os.Remove(f.Name())) }() 309 | 310 | res, err := New("bash", "-c", ">&2 echo stderr; echo stdout"). 311 | AddOutputWriter(f).RunSuccessOutput() 312 | require.NoError(t, err) 313 | 314 | content, err := os.ReadFile(f.Name()) 315 | require.NoError(t, err) 316 | require.Equal(t, res.Output(), string(content)) 317 | } 318 | 319 | func TestCommandsSuccess(t *testing.T) { 320 | res, err := New("echo", "1").Verbose(). 321 | Add("echo", "2").Add("echo", "3").Run() 322 | require.NoError(t, err) 323 | require.True(t, res.Success()) 324 | require.Zero(t, res.ExitCode()) 325 | require.Contains(t, res.Output(), "1") 326 | require.Contains(t, res.Output(), "2") 327 | require.Contains(t, res.Output(), "3") 328 | } 329 | 330 | func TestCommandsFailure(t *testing.T) { 331 | res, err := New("echo", "1").Add("wrong").Add("echo", "3").Run() 332 | require.Error(t, err) 333 | require.Nil(t, res) 334 | } 335 | 336 | func TestEnv(t *testing.T) { 337 | t.Setenv("ABC", "test") // preserved 338 | t.Setenv("FOO", "test") // overwritten 339 | 340 | res, err := New("sh", "-c", "echo $TEST; echo $FOO; echo $ABC"). 341 | Env("TEST=123"). 342 | Env("FOO=bar"). 343 | RunSuccessOutput() 344 | require.NoError(t, err) 345 | require.Equal(t, "123\nbar\ntest", res.OutputTrimNL()) 346 | } 347 | 348 | func TestFilterStdout(t *testing.T) { 349 | cmd, err := New("echo", "-n", "1 2 2 3").Filter("[25]", "0") 350 | require.NoError(t, err) 351 | 352 | res, err := cmd.Add("echo", "-n", "4 5 6 2 2").Run() 353 | require.NoError(t, err) 354 | require.True(t, res.Success()) 355 | require.Zero(t, res.ExitCode()) 356 | require.Equal(t, "\n1 0 0 3\n4 0 6 0 0", res.Output()) 357 | } 358 | 359 | func TestFilterStderr(t *testing.T) { 360 | res, err := New("bash", "-c", ">&2 echo -n my secret").Filter("secret", "***") 361 | require.NoError(t, err) 362 | out, err := res.RunSilentSuccessOutput() 363 | require.NoError(t, err) 364 | require.Equal(t, "my ***", out.Error()) 365 | require.Empty(t, out.Output()) 366 | } 367 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /http/http_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package http_test 18 | 19 | import ( 20 | "bytes" 21 | "errors" 22 | "fmt" 23 | "io" 24 | "net/http" 25 | "net/http/httptest" 26 | "strings" 27 | "testing" 28 | 29 | "github.com/stretchr/testify/require" 30 | 31 | khttp "sigs.k8s.io/release-utils/http" 32 | "sigs.k8s.io/release-utils/http/httpfakes" 33 | ) 34 | 35 | func TestGetURLResponseSuccess(t *testing.T) { 36 | // Given 37 | server := httptest.NewServer(http.HandlerFunc( 38 | func(w http.ResponseWriter, _ *http.Request) { 39 | _, err := io.WriteString(w, "") 40 | if err != nil { 41 | t.Fail() 42 | } 43 | })) 44 | defer server.Close() 45 | 46 | // When 47 | actual, err := khttp.GetURLResponse(server.URL, false) 48 | 49 | // Then 50 | require.NoError(t, err) 51 | require.Empty(t, actual) 52 | } 53 | 54 | func TestGetURLResponseSuccessTrimmed(t *testing.T) { 55 | // Given 56 | const expected = " some test " 57 | 58 | server := httptest.NewServer(http.HandlerFunc( 59 | func(w http.ResponseWriter, _ *http.Request) { 60 | _, err := io.WriteString(w, expected) 61 | if err != nil { 62 | t.Fail() 63 | } 64 | })) 65 | defer server.Close() 66 | 67 | // When 68 | actual, err := khttp.GetURLResponse(server.URL, true) 69 | 70 | // Then 71 | require.NoError(t, err) 72 | require.Equal(t, strings.TrimSpace(expected), actual) 73 | } 74 | 75 | func TestGetURLResponseFailedStatus(t *testing.T) { 76 | // Given 77 | server := httptest.NewServer(http.HandlerFunc( 78 | func(w http.ResponseWriter, _ *http.Request) { 79 | w.WriteHeader(http.StatusBadRequest) 80 | })) 81 | defer server.Close() 82 | 83 | // When 84 | _, err := khttp.GetURLResponse(server.URL, true) 85 | 86 | // Then 87 | require.Error(t, err) 88 | } 89 | 90 | func NewTestAgent() *khttp.Agent { 91 | agent := khttp.NewAgent() 92 | agent.SetImplementation(&httpfakes.FakeAgentImplementation{}) 93 | 94 | return agent 95 | } 96 | 97 | func TestAgentGetToWriter(t *testing.T) { 98 | agent := NewTestAgent() 99 | 100 | for _, tc := range []struct { 101 | n string 102 | prepare func(*httpfakes.FakeAgentImplementation, *http.Response) 103 | mustErr bool 104 | }{ 105 | { 106 | n: "success", 107 | prepare: func(fake *httpfakes.FakeAgentImplementation, resp *http.Response) { 108 | fake.SendGetRequestReturns(resp, nil) 109 | }, 110 | }, 111 | { 112 | n: "fail", 113 | prepare: func(fake *httpfakes.FakeAgentImplementation, resp *http.Response) { 114 | fake.SendGetRequestReturns(resp, errors.New("HTTP Post error")) 115 | }, 116 | mustErr: true, 117 | }, 118 | } { 119 | t.Run(tc.n, func(t *testing.T) { 120 | // First simulate a successful request 121 | fake := &httpfakes.FakeAgentImplementation{} 122 | 123 | resp := getTestResponse() 124 | defer resp.Body.Close() 125 | 126 | tc.prepare(fake, resp) 127 | 128 | var buf bytes.Buffer 129 | 130 | agent.SetImplementation(fake) 131 | 132 | err := agent.GetToWriter(&buf, "http://www.example.com/") 133 | if tc.mustErr { 134 | require.Error(t, err) 135 | 136 | return 137 | } 138 | 139 | require.NoError(t, err) 140 | require.Equal(t, buf.Bytes(), []byte("hello sig-release!")) 141 | }) 142 | } 143 | } 144 | 145 | func TestAgentHead(t *testing.T) { 146 | t.Parallel() 147 | 148 | agent := NewTestAgent().WithRetries(0) 149 | 150 | resp := getTestResponse() 151 | defer resp.Body.Close() 152 | 153 | // First simulate a successful request 154 | fake := &httpfakes.FakeAgentImplementation{} 155 | fake.SendHeadRequestReturns(resp, nil) 156 | 157 | agent.SetImplementation(fake) 158 | b, err := agent.Head("http://www.example.com/") 159 | require.NoError(t, err) 160 | require.Equal(t, b, []byte("hello sig-release!")) 161 | 162 | // Now check error is handled 163 | fake.SendHeadRequestReturns(resp, errors.New("HTTP Head error")) 164 | agent.SetImplementation(fake) 165 | _, err = agent.Head("http://www.example.com/") 166 | require.Error(t, err) 167 | } 168 | 169 | func getTestResponse() *http.Response { 170 | return &http.Response{ 171 | Status: "200 OK", 172 | StatusCode: http.StatusOK, 173 | Body: io.NopCloser(bytes.NewReader([]byte("hello sig-release!"))), 174 | ContentLength: 18, 175 | Close: true, 176 | Request: &http.Request{}, 177 | } 178 | } 179 | 180 | func TestAgentPostToWriter(t *testing.T) { 181 | for _, tc := range []struct { 182 | n string 183 | prepare func(*httpfakes.FakeAgentImplementation, *http.Response) 184 | mustErr bool 185 | }{ 186 | { 187 | n: "success", 188 | prepare: func(fake *httpfakes.FakeAgentImplementation, resp *http.Response) { 189 | fake.SendPostRequestReturns(resp, nil) 190 | }, 191 | }, 192 | { 193 | n: "fail", 194 | prepare: func(fake *httpfakes.FakeAgentImplementation, resp *http.Response) { 195 | fake.SendPostRequestReturns(resp, errors.New("HTTP Post error")) 196 | }, 197 | mustErr: true, 198 | }, 199 | } { 200 | t.Run(tc.n, func(t *testing.T) { 201 | agent := NewTestAgent() 202 | // First simulate a successful request 203 | fake := &httpfakes.FakeAgentImplementation{} 204 | 205 | resp := getTestResponse() 206 | defer resp.Body.Close() 207 | 208 | tc.prepare(fake, resp) 209 | 210 | var buf bytes.Buffer 211 | 212 | agent.SetImplementation(fake) 213 | 214 | err := agent.PostToWriter(&buf, "http://www.example.com/", []byte{}) 215 | if tc.mustErr { 216 | require.Error(t, err) 217 | 218 | return 219 | } 220 | 221 | require.NoError(t, err) 222 | require.Equal(t, buf.Bytes(), []byte("hello sig-release!")) 223 | }) 224 | } 225 | } 226 | 227 | func TestAgentOptions(t *testing.T) { 228 | agent := NewTestAgent() 229 | fake := &httpfakes.FakeAgentImplementation{} 230 | 231 | resp := &http.Response{ 232 | Status: "Fake not found", 233 | StatusCode: http.StatusNotFound, 234 | Body: io.NopCloser(bytes.NewReader([]byte("hello sig-release!"))), 235 | ContentLength: 18, 236 | Close: true, 237 | Request: &http.Request{}, 238 | } 239 | defer resp.Body.Close() 240 | 241 | fake.SendGetRequestReturns(resp, nil) 242 | agent.SetImplementation(fake) 243 | 244 | // Test FailOnHTTPError 245 | // First we fail on server errors 246 | _, err := agent.WithFailOnHTTPError(true).Get("http://example.com/") 247 | require.Error(t, err) 248 | 249 | // Then we just note them and do not fail 250 | _, err = agent.WithFailOnHTTPError(false).Get("http://example.com/") 251 | require.NoError(t, err) 252 | } 253 | 254 | // closeHTTPResponseGroup is an internal func that closes the response bodies. 255 | func closeHTTPResponseGroup(resps []*http.Response) { 256 | for i := range resps { 257 | if resps[i] == nil { 258 | continue 259 | } 260 | 261 | resps[i].Body.Close() 262 | } 263 | } 264 | 265 | func TestAgentGroupGetRequest(t *testing.T) { 266 | fake := &httpfakes.FakeAgentImplementation{} 267 | fakeUrls := []string{"http://www/1", "http://www/2", "http://www/3"} 268 | 269 | fake.SendGetRequestCalls(func(_ *http.Client, s string) (*http.Response, error) { 270 | switch s { 271 | case fakeUrls[0]: 272 | return &http.Response{ 273 | Status: "Fake OK", 274 | StatusCode: http.StatusOK, 275 | Body: io.NopCloser(bytes.NewReader([]byte("hello sig-release!"))), 276 | ContentLength: 18, 277 | Close: true, 278 | Request: &http.Request{}, 279 | }, nil 280 | case fakeUrls[1]: 281 | return &http.Response{ 282 | Status: "Fake not found", 283 | StatusCode: http.StatusNotFound, 284 | Body: io.NopCloser(bytes.NewReader([]byte("hello sig-release!"))), 285 | ContentLength: 18, 286 | Close: true, 287 | Request: &http.Request{}, 288 | }, nil 289 | case fakeUrls[2]: 290 | return nil, errors.New("malformed url") 291 | } 292 | 293 | return nil, nil 294 | }) 295 | 296 | for _, tc := range []struct { 297 | name string 298 | workers int 299 | }{ 300 | {"no-parallelism", 1}, {"one-per-request", 3}, {"spare-workers", 5}, 301 | } { 302 | t.Run(tc.name, func(t *testing.T) { 303 | // No retries as the errors are synthetic 304 | agent := NewTestAgent().WithRetries(0).WithFailOnHTTPError(false).WithMaxParallel(tc.workers) 305 | agent.SetImplementation(fake) 306 | 307 | //nolint: bodyclose // The next line closes them 308 | resps, errs := agent.GetRequestGroup(fakeUrls) 309 | defer closeHTTPResponseGroup(resps) 310 | 311 | require.Len(t, resps, 3) 312 | require.Len(t, errs, 3) 313 | 314 | require.NoError(t, errs[0]) 315 | require.NoError(t, errs[1]) 316 | require.Error(t, errs[2]) 317 | 318 | require.Equal(t, http.StatusOK, resps[0].StatusCode) 319 | require.Equal(t, http.StatusNotFound, resps[1].StatusCode) 320 | require.Nil(t, resps[2]) 321 | }) 322 | } 323 | } 324 | 325 | func TestAgentPostRequestGroup(t *testing.T) { 326 | t.Parallel() 327 | 328 | fake := &httpfakes.FakeAgentImplementation{} 329 | errorURL := "fake:error" 330 | httpErrorURL := "fake:httpError" 331 | noErrorURL := "fake:ok" 332 | 333 | fake.SendPostRequestCalls(func(_ *http.Client, s string, _ []byte, _ string) (*http.Response, error) { 334 | switch s { 335 | case noErrorURL: 336 | return &http.Response{ 337 | Status: "Fake OK", 338 | StatusCode: http.StatusOK, 339 | Body: io.NopCloser(bytes.NewReader([]byte("hello sig-release!"))), 340 | ContentLength: 18, 341 | Close: true, 342 | Request: &http.Request{}, 343 | }, nil 344 | case httpErrorURL: 345 | return &http.Response{ 346 | Status: "Fake not found", 347 | StatusCode: http.StatusNotFound, 348 | Body: io.NopCloser(bytes.NewReader([]byte("hello sig-release!"))), 349 | ContentLength: 18, 350 | Close: true, 351 | Request: &http.Request{}, 352 | }, fmt.Errorf("HTTP error %d for %s", http.StatusNotFound, s) 353 | case errorURL: 354 | return nil, errors.New("malformed url") 355 | } 356 | 357 | return nil, nil 358 | }) 359 | 360 | for _, tc := range []struct { 361 | name string 362 | workers int 363 | mustErr bool 364 | urls []string 365 | postData [][]byte 366 | }{ 367 | {"no-parallelism", 1, false, []string{noErrorURL, noErrorURL, noErrorURL}, make([][]byte, 3)}, 368 | {"one-per-request", 3, false, []string{noErrorURL, noErrorURL, noErrorURL}, make([][]byte, 3)}, 369 | {"spare-workers", 5, false, []string{noErrorURL, noErrorURL, noErrorURL}, make([][]byte, 3)}, 370 | {"uneven-postdata", 5, true, []string{noErrorURL, noErrorURL, noErrorURL}, make([][]byte, 2)}, 371 | {"uneven-postdata2", 5, true, []string{noErrorURL, noErrorURL, noErrorURL}, make([][]byte, 4)}, 372 | {"http-error", 5, true, []string{noErrorURL, httpErrorURL, noErrorURL}, make([][]byte, 3)}, 373 | {"software-error", 5, true, []string{noErrorURL, errorURL, noErrorURL}, make([][]byte, 3)}, 374 | } { 375 | t.Run(tc.name, func(t *testing.T) { 376 | t.Parallel() 377 | // No retries as the errors are synthetic 378 | agent := NewTestAgent().WithRetries(0).WithFailOnHTTPError(false).WithMaxParallel(tc.workers) 379 | agent.SetImplementation(fake) 380 | 381 | //nolint: bodyclose 382 | resps, errs := agent.PostRequestGroup(tc.urls, tc.postData) 383 | closeHTTPResponseGroup(resps) 384 | 385 | // If urls and postdata don't all errors should be errors 386 | if len(tc.urls) != len(tc.postData) { 387 | for i := range errs { 388 | require.Error(t, errs[i]) 389 | } 390 | 391 | return 392 | } 393 | 394 | // Check for at least on error 395 | if tc.mustErr { 396 | require.Error(t, errors.Join(errs...)) 397 | } else { 398 | require.NoError(t, errors.Join(errs...)) 399 | } 400 | 401 | require.Len(t, resps, len(tc.urls)) 402 | require.Len(t, errs, len(tc.urls)) 403 | 404 | for i := range tc.urls { 405 | switch tc.urls[i] { 406 | case noErrorURL: 407 | require.NoError(t, errs[i]) 408 | require.NotNil(t, resps[i]) 409 | require.Equal(t, http.StatusOK, resps[i].StatusCode) 410 | case httpErrorURL: 411 | require.Error(t, errs[i]) 412 | require.NotNil(t, resps[i]) 413 | require.Equal(t, http.StatusNotFound, resps[i].StatusCode) 414 | case errorURL: 415 | require.Error(t, errs[i]) 416 | } 417 | } 418 | }) 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /command/command.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package command 18 | 19 | import ( 20 | "bytes" 21 | "errors" 22 | "fmt" 23 | "io" 24 | "os" 25 | "os/exec" 26 | "regexp" 27 | "strings" 28 | "sync" 29 | "syscall" 30 | 31 | "github.com/sirupsen/logrus" 32 | ) 33 | 34 | // A generic command abstraction. 35 | type Command struct { 36 | cmds []*command 37 | stdErrWriters, stdOutWriters []io.Writer 38 | env []string 39 | verbose bool 40 | filter *filter 41 | } 42 | 43 | // The internal command representation. 44 | type command struct { 45 | *exec.Cmd 46 | pipeWriter *io.PipeWriter 47 | } 48 | 49 | // filter is the internally used struct for filtering command output. 50 | type filter struct { 51 | regex *regexp.Regexp 52 | replaceAll string 53 | } 54 | 55 | // A generic command exit status. 56 | type Status struct { //nolint: errname 57 | waitStatus syscall.WaitStatus 58 | *Stream 59 | } 60 | 61 | // Stream combines standard output and error. 62 | type Stream struct { //nolint: errname 63 | stdOut string 64 | stdErr string 65 | } 66 | 67 | // Commands is an abstraction over multiple Command structures. 68 | type Commands []*Command 69 | 70 | // New creates a new command from the provided arguments. 71 | func New(cmd string, args ...string) *Command { 72 | return NewWithWorkDir("", cmd, args...) 73 | } 74 | 75 | // NewWithWorkDir creates a new command from the provided workDir and the command 76 | // arguments. 77 | func NewWithWorkDir(workDir, cmd string, args ...string) *Command { 78 | return &Command{ 79 | cmds: []*command{{ 80 | Cmd: cmdWithDir(workDir, cmd, args...), 81 | pipeWriter: nil, 82 | }}, 83 | stdErrWriters: []io.Writer{}, 84 | stdOutWriters: []io.Writer{}, 85 | verbose: false, 86 | } 87 | } 88 | 89 | func cmdWithDir(dir, cmd string, args ...string) *exec.Cmd { 90 | c := exec.Command(cmd, args...) 91 | c.Dir = dir 92 | 93 | return c 94 | } 95 | 96 | // Pipe creates a new command where the previous should be piped to. 97 | func (c *Command) Pipe(cmd string, args ...string) *Command { 98 | pipeCmd := cmdWithDir(c.cmds[0].Dir, cmd, args...) 99 | 100 | reader, writer := io.Pipe() 101 | c.cmds[len(c.cmds)-1].Stdout = writer 102 | pipeCmd.Stdin = reader 103 | 104 | c.cmds = append(c.cmds, &command{ 105 | Cmd: pipeCmd, 106 | pipeWriter: writer, 107 | }) 108 | 109 | return c 110 | } 111 | 112 | // Env specifies the environment added to the command. Each entry is of the 113 | // form "key=value". The environment of the current process is being preserved, 114 | // while it is possible to overwrite already existing environment variables. 115 | func (c *Command) Env(env ...string) *Command { 116 | c.env = append(c.env, env...) 117 | 118 | return c 119 | } 120 | 121 | // Verbose enables verbose output aka printing the command before executing it. 122 | func (c *Command) Verbose() *Command { 123 | c.verbose = true 124 | 125 | return c 126 | } 127 | 128 | // isVerbose returns true if the command is in verbose mode, either set locally 129 | // or global. 130 | func (c *Command) isVerbose() bool { 131 | return GetGlobalVerbose() || c.verbose 132 | } 133 | 134 | // Add a command with the same working directory as well as verbosity mode. 135 | // Returns a new Commands instance. 136 | func (c *Command) Add(cmd string, args ...string) Commands { 137 | addCmd := NewWithWorkDir(c.cmds[0].Dir, cmd, args...) 138 | addCmd.verbose = c.verbose 139 | addCmd.filter = c.filter 140 | 141 | return Commands{c, addCmd} 142 | } 143 | 144 | // AddWriter can be used to add an additional output (stdout) and error 145 | // (stderr) writer to the command, for example when having the need to log to 146 | // files. 147 | func (c *Command) AddWriter(writer io.Writer) *Command { 148 | c.AddOutputWriter(writer) 149 | c.AddErrorWriter(writer) 150 | 151 | return c 152 | } 153 | 154 | // AddErrorWriter can be used to add an additional error (stderr) writer to the 155 | // command, for example when having the need to log to files. 156 | func (c *Command) AddErrorWriter(writer io.Writer) *Command { 157 | c.stdErrWriters = append(c.stdErrWriters, writer) 158 | 159 | return c 160 | } 161 | 162 | // AddOutputWriter can be used to add an additional output (stdout) writer to 163 | // the command, for example when having the need to log to files. 164 | func (c *Command) AddOutputWriter(writer io.Writer) *Command { 165 | c.stdOutWriters = append(c.stdOutWriters, writer) 166 | 167 | return c 168 | } 169 | 170 | // Filter adds an output filter regular expression to the command. Every output 171 | // will then be replaced with the string provided by replaceAll. 172 | func (c *Command) Filter(regex, replaceAll string) (*Command, error) { 173 | filterRegex, err := regexp.Compile(regex) 174 | if err != nil { 175 | return nil, fmt.Errorf("compile regular expression: %w", err) 176 | } 177 | 178 | c.filter = &filter{ 179 | regex: filterRegex, 180 | replaceAll: replaceAll, 181 | } 182 | 183 | return c, nil 184 | } 185 | 186 | // Run starts the command and waits for it to finish. It returns an error if 187 | // the command execution was not possible at all, otherwise the Status. 188 | // This method prints the commands output during execution. 189 | func (c *Command) Run() (res *Status, err error) { 190 | return c.run(true) 191 | } 192 | 193 | // RunSuccessOutput starts the command and waits for it to finish. It returns 194 | // an error if the command execution was not successful, otherwise its output. 195 | func (c *Command) RunSuccessOutput() (output *Stream, err error) { 196 | res, err := c.run(true) 197 | if err != nil { 198 | return nil, err 199 | } 200 | 201 | if !res.Success() { 202 | return nil, fmt.Errorf("command %v did not succeed: %v", c.String(), res.Error()) 203 | } 204 | 205 | return res.Stream, nil 206 | } 207 | 208 | // RunSuccess starts the command and waits for it to finish. It returns an 209 | // error if the command execution was not successful. 210 | func (c *Command) RunSuccess() error { 211 | _, err := c.RunSuccessOutput() 212 | 213 | return err 214 | } 215 | 216 | // String returns a string representation of the full command. 217 | func (c *Command) String() string { 218 | str := []string{} 219 | 220 | for _, x := range c.cmds { 221 | // Note: the following logic can be replaced with x.String(), which was 222 | // implemented in go1.13 223 | b := new(strings.Builder) 224 | b.WriteString(x.Path) 225 | 226 | for _, a := range x.Args[1:] { 227 | b.WriteByte(' ') 228 | b.WriteString(a) 229 | } 230 | 231 | str = append(str, b.String()) 232 | } 233 | 234 | return strings.Join(str, " | ") 235 | } 236 | 237 | // Run starts the command and waits for it to finish. It returns an error if 238 | // the command execution was not possible at all, otherwise the Status. 239 | // This method does not print the output of the command during its execution. 240 | func (c *Command) RunSilent() (res *Status, err error) { 241 | return c.run(false) 242 | } 243 | 244 | // RunSilentSuccessOutput starts the command and waits for it to finish. It 245 | // returns an error if the command execution was not successful, otherwise its 246 | // output. This method does not print the output of the command during its 247 | // execution. 248 | func (c *Command) RunSilentSuccessOutput() (output *Stream, err error) { 249 | res, err := c.run(false) 250 | if err != nil { 251 | return nil, err 252 | } 253 | 254 | if !res.Success() { 255 | return nil, fmt.Errorf("command %v did not succeed: %w", c.String(), res) 256 | } 257 | 258 | return res.Stream, nil 259 | } 260 | 261 | // RunSilentSuccess starts the command and waits for it to finish. It returns 262 | // an error if the command execution was not successful. This method does not 263 | // print the output of the command during its execution. 264 | func (c *Command) RunSilentSuccess() error { 265 | _, err := c.RunSilentSuccessOutput() 266 | 267 | return err 268 | } 269 | 270 | // run is the internal run method. 271 | func (c *Command) run(printOutput bool) (res *Status, err error) { 272 | var runErr error 273 | 274 | stdOutBuffer := &bytes.Buffer{} 275 | stdErrBuffer := &bytes.Buffer{} 276 | status := &Status{Stream: &Stream{}} 277 | 278 | type done struct { 279 | stdout error 280 | stderr error 281 | } 282 | 283 | doneChan := make(chan done, 1) 284 | 285 | var stdOutWriter io.Writer 286 | 287 | for i, cmd := range c.cmds { 288 | // Last command handling 289 | if i+1 == len(c.cmds) { 290 | stdout, err := cmd.StdoutPipe() 291 | if err != nil { 292 | return nil, err 293 | } 294 | 295 | stderr, err := cmd.StderrPipe() 296 | if err != nil { 297 | return nil, err 298 | } 299 | 300 | var stdErrWriter io.Writer 301 | 302 | if printOutput { 303 | stdOutWriter = io.MultiWriter(append( 304 | []io.Writer{os.Stdout, stdOutBuffer}, c.stdOutWriters..., 305 | )...) 306 | stdErrWriter = io.MultiWriter(append( 307 | []io.Writer{os.Stderr, stdErrBuffer}, c.stdErrWriters..., 308 | )...) 309 | } else { 310 | stdOutWriter = stdOutBuffer 311 | stdErrWriter = stdErrBuffer 312 | } 313 | 314 | go func() { 315 | var stdoutErr, stderrErr error 316 | 317 | wg := sync.WaitGroup{} 318 | 319 | wg.Add(2) 320 | 321 | filterCopy := func(read io.ReadCloser, write io.Writer) (err error) { 322 | if c.filter != nil { 323 | builder := &strings.Builder{} 324 | 325 | _, err = io.Copy(builder, read) 326 | if err != nil { 327 | return err 328 | } 329 | 330 | str := c.filter.regex.ReplaceAllString( 331 | builder.String(), c.filter.replaceAll, 332 | ) 333 | _, err = io.Copy(write, strings.NewReader(str)) 334 | } else { 335 | _, err = io.Copy(write, read) 336 | } 337 | 338 | return err 339 | } 340 | 341 | go func() { 342 | stdoutErr = filterCopy(stdout, stdOutWriter) 343 | 344 | wg.Done() 345 | }() 346 | 347 | go func() { 348 | stderrErr = filterCopy(stderr, stdErrWriter) 349 | 350 | wg.Done() 351 | }() 352 | 353 | wg.Wait() 354 | 355 | doneChan <- done{stdoutErr, stderrErr} 356 | }() 357 | } 358 | 359 | if c.isVerbose() { 360 | logrus.Infof("+ %s", c.String()) 361 | } 362 | 363 | cmd.Env = append(os.Environ(), c.env...) 364 | 365 | if err := cmd.Start(); err != nil { 366 | return nil, err 367 | } 368 | 369 | if i > 0 { 370 | if err := c.cmds[i-1].Wait(); err != nil { 371 | return nil, err 372 | } 373 | } 374 | 375 | if cmd.pipeWriter != nil { 376 | if err := cmd.pipeWriter.Close(); err != nil { 377 | return nil, err 378 | } 379 | } 380 | 381 | // Wait for last command in the pipe to finish 382 | if i+1 == len(c.cmds) { 383 | err := <-doneChan 384 | if err.stdout != nil && strings.Contains(err.stdout.Error(), os.ErrClosed.Error()) { 385 | return nil, fmt.Errorf("unable to copy stdout: %w", err.stdout) 386 | } 387 | 388 | if err.stderr != nil && strings.Contains(err.stderr.Error(), os.ErrClosed.Error()) { 389 | return nil, fmt.Errorf("unable to copy stderr: %w", err.stderr) 390 | } 391 | 392 | runErr = cmd.Wait() 393 | } 394 | } 395 | 396 | status.stdOut = stdOutBuffer.String() 397 | status.stdErr = stdErrBuffer.String() 398 | 399 | exitErr := &exec.ExitError{} 400 | if errors.As(runErr, &exitErr) { 401 | if waitStatus, ok := exitErr.Sys().(syscall.WaitStatus); ok { 402 | status.waitStatus = waitStatus 403 | 404 | return status, nil 405 | } 406 | } 407 | 408 | return status, runErr 409 | } 410 | 411 | // Success returns if a Status was successful. 412 | func (s *Status) Success() bool { 413 | return s.waitStatus.ExitStatus() == 0 414 | } 415 | 416 | // ExitCode returns the exit status of the command status. 417 | func (s *Status) ExitCode() int { 418 | return s.waitStatus.ExitStatus() 419 | } 420 | 421 | // Output returns stdout of the command status. 422 | func (s *Stream) Output() string { 423 | return s.stdOut 424 | } 425 | 426 | // OutputTrimNL returns stdout of the command status with newlines trimmed 427 | // Use only when output is expected to be a single "word", like a version string. 428 | func (s *Stream) OutputTrimNL() string { 429 | return strings.TrimSpace(s.stdOut) 430 | } 431 | 432 | // Error returns the stderr of the command status. 433 | func (s *Stream) Error() string { 434 | return s.stdErr 435 | } 436 | 437 | // Execute is a convenience function which creates a new Command, executes it 438 | // and evaluates its status. 439 | func Execute(cmd string, args ...string) error { 440 | status, err := New(cmd, args...).Run() 441 | if err != nil { 442 | return fmt.Errorf("command %q is not executable: %w", cmd, err) 443 | } 444 | 445 | if !status.Success() { 446 | return fmt.Errorf( 447 | "command %q did not exit successful (%d)", 448 | cmd, status.ExitCode(), 449 | ) 450 | } 451 | 452 | return nil 453 | } 454 | 455 | // Available verifies that the specified `commands` are available within the 456 | // current `$PATH` environment and returns true if so. The function does not 457 | // check for duplicates nor if the provided slice is empty. 458 | func Available(commands ...string) (ok bool) { 459 | ok = true 460 | 461 | for _, command := range commands { 462 | if _, err := exec.LookPath(command); err != nil { 463 | logrus.Warnf("Unable to %v", err) 464 | 465 | ok = false 466 | } 467 | } 468 | 469 | return ok 470 | } 471 | 472 | // Add adds another command with the same working directory as well as 473 | // verbosity mode to the Commands. 474 | func (c Commands) Add(cmd string, args ...string) Commands { 475 | addCmd := NewWithWorkDir(c[0].cmds[0].Dir, cmd, args...) 476 | addCmd.verbose = c[0].verbose 477 | addCmd.filter = c[0].filter 478 | 479 | return append(c, addCmd) 480 | } 481 | 482 | // Run executes all commands sequentially and abort if any of those fails. 483 | func (c Commands) Run() (*Status, error) { 484 | res := &Status{Stream: &Stream{}} 485 | 486 | for _, cmd := range c { 487 | output, err := cmd.RunSuccessOutput() 488 | if err != nil { 489 | return nil, fmt.Errorf("running command %q: %w", cmd.String(), err) 490 | } 491 | 492 | res.stdOut += "\n" + output.stdOut 493 | res.stdErr += "\n" + output.stdErr 494 | } 495 | 496 | return res, nil 497 | } 498 | --------------------------------------------------------------------------------