mirror of
https://github.com/helm/chart-testing.git
synced 2026-02-05 18:45:18 +01:00
see https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#grouping-log-lines Signed-off-by: Joe Julian <me@joejulian.name>
253 lines
6.5 KiB
Go
253 lines
6.5 KiB
Go
// Copyright The Helm Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package util
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"math/rand"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Masterminds/semver"
|
|
"github.com/hashicorp/go-multierror"
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
const chars = "1234567890abcdefghijklmnopqrstuvwxyz"
|
|
|
|
type Maintainer struct {
|
|
Name string `yaml:"name"`
|
|
Email string `yaml:"email"`
|
|
}
|
|
|
|
type ChartYaml struct {
|
|
Name string `yaml:"name"`
|
|
Version string `yaml:"version"`
|
|
Deprecated bool `yaml:"deprecated"`
|
|
Maintainers []Maintainer
|
|
}
|
|
|
|
func Flatten(items []interface{}) ([]string, error) {
|
|
return doFlatten([]string{}, items)
|
|
}
|
|
|
|
func init() {
|
|
rand.New(rand.NewSource(time.Now().UnixNano())) // nolint: gosec
|
|
}
|
|
|
|
func doFlatten(result []string, items interface{}) ([]string, error) {
|
|
var err error
|
|
|
|
switch v := items.(type) {
|
|
case string:
|
|
result = append(result, v)
|
|
case []string:
|
|
result = append(result, v...)
|
|
case []interface{}:
|
|
for _, item := range v {
|
|
result, err = doFlatten(result, item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("flatten does not support %T", v)
|
|
}
|
|
|
|
return result, err
|
|
}
|
|
|
|
func StringSliceContains(slice []string, s string) bool {
|
|
for _, element := range slice {
|
|
if s == element {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func FileExists(file string) bool {
|
|
if _, err := os.Stat(file); err != nil {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// RandomString string creates a random string of numbers and lower-case ascii characters with the specified length.
|
|
func RandomString(length int) string {
|
|
n := len(chars)
|
|
bytes := make([]byte, length)
|
|
for i := range bytes {
|
|
bytes[i] = chars[rand.Intn(n)] // nolint: gosec
|
|
}
|
|
return string(bytes)
|
|
}
|
|
|
|
type DirectoryLister struct{}
|
|
|
|
// ListChildDirs lists subdirectories of parentDir matching the test function.
|
|
func (l DirectoryLister) ListChildDirs(parentDir string, test func(dir string) bool) ([]string, error) {
|
|
entries, err := os.ReadDir(parentDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fileInfos := make([]fs.FileInfo, 0, len(entries))
|
|
for _, entry := range entries {
|
|
info, err := entry.Info()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fileInfos = append(fileInfos, info)
|
|
}
|
|
|
|
var dirs []string
|
|
for _, dir := range fileInfos {
|
|
dirName := dir.Name()
|
|
parentSlashChildDir := filepath.Join(parentDir, dirName)
|
|
if test(parentSlashChildDir) {
|
|
dirs = append(dirs, parentSlashChildDir)
|
|
}
|
|
}
|
|
|
|
return dirs, nil
|
|
}
|
|
|
|
type Utils struct{}
|
|
|
|
func (u Utils) LookupChartDir(chartDirs []string, dir string) (string, error) {
|
|
for _, chartDir := range chartDirs {
|
|
currentDir := dir
|
|
for {
|
|
chartYaml := filepath.Join(currentDir, "Chart.yaml")
|
|
parent := filepath.Dir(filepath.Dir(chartYaml))
|
|
chartDir = strings.TrimRight(chartDir, "/") // remove any trailing slash from the dir
|
|
|
|
// check directory has a Chart.yaml and that it is in a
|
|
// direct subdirectory of a configured charts directory
|
|
if FileExists(chartYaml) && (parent == chartDir) {
|
|
return currentDir, nil
|
|
}
|
|
|
|
currentDir = filepath.Dir(currentDir)
|
|
relativeDir, _ := filepath.Rel(chartDir, currentDir)
|
|
joined := filepath.Join(chartDir, relativeDir)
|
|
if (joined == chartDir) || strings.HasPrefix(relativeDir, "..") {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return "", errors.New("no chart directory")
|
|
}
|
|
|
|
// ReadChartYaml attempts to parse Chart.yaml within the specified directory
|
|
// and return a newly allocated ChartYaml object. If no Chart.yaml is present
|
|
// or there is an error unmarshaling the file contents, an error will be returned.
|
|
func ReadChartYaml(dir string) (*ChartYaml, error) {
|
|
yamlBytes, err := os.ReadFile(filepath.Join(dir, "Chart.yaml"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not read 'Chart.yaml': %w", err)
|
|
}
|
|
return UnmarshalChartYaml(yamlBytes)
|
|
}
|
|
|
|
// UnmarshalChartYaml parses the yaml encoded data and returns a newly
|
|
// allocated ChartYaml object.
|
|
func UnmarshalChartYaml(yamlBytes []byte) (*ChartYaml, error) {
|
|
chartYaml := &ChartYaml{}
|
|
if err := yaml.Unmarshal(yamlBytes, chartYaml); err != nil {
|
|
return nil, fmt.Errorf("could not unmarshal 'Chart.yaml': %w", err)
|
|
}
|
|
return chartYaml, nil
|
|
}
|
|
|
|
func CompareVersions(left string, right string) (int, error) {
|
|
leftVersion, err := semver.NewVersion(left)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed parsing semantic version: %w", err)
|
|
}
|
|
rightVersion, err := semver.NewVersion(right)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed parsing semantic version: %w", err)
|
|
}
|
|
return leftVersion.Compare(rightVersion), nil
|
|
}
|
|
|
|
func BreakingChangeAllowed(left string, right string) (bool, error) {
|
|
leftVersion, err := semver.NewVersion(left)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed parsing semantic version: %w", err)
|
|
}
|
|
rightVersion, err := semver.NewVersion(right)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed parsing semantic version: %w", err)
|
|
}
|
|
|
|
constraintOp := "^"
|
|
if leftVersion.Major() == 0 {
|
|
constraintOp = "~"
|
|
}
|
|
c, err := semver.NewConstraint(fmt.Sprintf("%s %s", constraintOp, leftVersion.String()))
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed parsing semantic version constraint: %w", err)
|
|
}
|
|
|
|
minor, reasons := c.Validate(rightVersion)
|
|
if len(reasons) > 0 {
|
|
err = multierror.Append(err, reasons...)
|
|
}
|
|
|
|
return !minor, err
|
|
}
|
|
|
|
func PrintDelimiterLineToWriter(w io.Writer, delimiterChar string) {
|
|
fmt.Fprintln(w, strings.Repeat(delimiterChar, 120))
|
|
}
|
|
|
|
func GithubGroupsBegin(w io.Writer, title string) {
|
|
fmt.Fprintf(w, "::group::%s\n", title)
|
|
}
|
|
|
|
func GithubGroupsEnd(w io.Writer) {
|
|
fmt.Fprintln(w, "::endgroup::")
|
|
}
|
|
|
|
func SanitizeName(s string, maxLength int) string {
|
|
reg := regexp.MustCompile("^[^a-zA-Z0-9]+")
|
|
|
|
excess := len(s) - maxLength
|
|
result := s
|
|
if excess > 0 {
|
|
result = s[excess:]
|
|
}
|
|
return reg.ReplaceAllString(result, "")
|
|
}
|
|
|
|
func GetRandomPort() (int, error) {
|
|
listener, err := net.Listen("tcp", ":0") // nolint: gosec
|
|
defer listener.Close() // nolint: staticcheck
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return listener.Addr().(*net.TCPAddr).Port, nil
|
|
}
|