1
0
mirror of https://github.com/helm/chart-testing.git synced 2026-02-05 18:45:18 +01:00
Files
chart-testing/pkg/chart/chart.go
Aleksey @soar Smyrnov 560d59deb1 Fix for --upgrade option (#624)
Signed-off-by: Aleksey @soar Smyrnov <i@soar.name>
2024-02-05 15:21:24 +01:00

982 lines
30 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 chart
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/Masterminds/semver"
"github.com/helm/chart-testing/v3/pkg/config"
"github.com/helm/chart-testing/v3/pkg/exec"
"github.com/helm/chart-testing/v3/pkg/tool"
"github.com/helm/chart-testing/v3/pkg/util"
)
const maxNameLength = 63
// Git is the Interface that wraps Git operations.
//
// FileExistsOnBranch checks whether file exists on the specified remote/branch.
//
// Show returns the contents of file on the specified remote/branch.
//
// AddWorktree checks out the contents of the repository at a commit ref into the specified path.
//
// RemoveWorktree removes the working tree at the specified path.
//
// MergeBase returns the SHA1 of the merge base of commit1 and commit2.
//
// ListChangedFilesInDirs diffs commit against HEAD and returns changed files for the specified dirs.
//
// GetURLForRemote returns the repo URL for the specified remote.
//
// ValidateRepository checks that the current working directory is a valid git repository,
// and returns nil if valid.
//
// BranchExists checks whether a given branch exists in the git repository.
type Git interface {
FileExistsOnBranch(file string, remote string, branch string) bool
Show(file string, remote string, branch string) (string, error)
AddWorktree(path string, ref string) error
RemoveWorktree(path string) error
MergeBase(commit1 string, commit2 string) (string, error)
ListChangedFilesInDirs(commit string, dirs ...string) ([]string, error)
GetURLForRemote(remote string) (string, error)
ValidateRepository() error
BranchExists(branch string) bool
}
// Helm is the interface that wraps Helm operations
//
// # AddRepo adds a chart repository to the local Helm configuration
//
// # BuildDependencies builds the chart's dependencies
//
// # BuildDependenciesWithArgs allows passing additional arguments to BuildDependencies
//
// LintWithValues runs `helm lint` for the given chart using the specified values file.
// Pass a zero value for valuesFile in order to run lint without specifying a values file.
//
// InstallWithValues runs `helm install` for the given chart using the specified values file.
// Pass a zero value for valuesFile in order to run install without specifying a values file.
//
// Upgrade runs `helm upgrade` against an existing release, and re-uses the previously computed values.
//
// Test runs `helm test` against an existing release. Set the cleanup argument to true in order
// to clean up test pods created by helm after the test command completes.
//
// DeleteRelease purges the specified Helm release.
type Helm interface {
AddRepo(name string, url string, extraArgs []string) error
BuildDependencies(chart string) error
BuildDependenciesWithArgs(chart string, extraArgs []string) error
LintWithValues(chart string, valuesFile string) error
InstallWithValues(chart string, valuesFile string, namespace string, release string) error
Upgrade(chart string, namespace string, release string) error
Test(namespace string, release string) error
DeleteRelease(namespace string, release string)
Version() (string, error)
}
// Kubectl is the interface that wraps kubectl operations
//
// # DeleteNamespace deletes a namespace
//
// # WaitForDeployments waits for a deployment to become ready
//
// # GetPodsforDeployment gets all pods for a deployment
//
// # GetPods gets pods for the given args
//
// # GetEvents prints all events for namespace
//
// # DescribePod prints the pod's description
//
// # Logs prints the logs of container
//
// # GetInitContainers gets all init containers of pod
//
// GetContainers gets all containers of pod
type Kubectl interface {
CreateNamespace(namespace string) error
DeleteNamespace(namespace string)
WaitForDeployments(namespace string, selector string) error
GetPodsforDeployment(namespace string, deployment string) ([]string, error)
GetPods(args ...string) ([]string, error)
GetEvents(namespace string) error
DescribePod(namespace string, pod string) error
Logs(namespace string, pod string, container string) error
GetInitContainers(namespace string, pod string) ([]string, error)
GetContainers(namespace string, pod string) ([]string, error)
}
// Linter is the interface that wrap linting operations
//
// # YamlLint runs `yamllint` on the specified file with the specified configuration
//
// Yamale runs `yamale` on the specified file with the specified schema file
type Linter interface {
YamlLint(yamlFile string, configFile string) error
Yamale(yamlFile string, schemaFile string) error
}
// CmdExecutor is the interface
//
// RunCommand renders cmdTemplate as go template using data and executes the resulting command
type CmdExecutor interface {
RunCommand(cmdTemplate string, data interface{}) error
}
// DirectoryLister is the interface
//
// ListChildDirs lists direct child directories of parentDir given they pass the test function
type DirectoryLister interface {
ListChildDirs(parentDir string, test func(string) bool) ([]string, error)
}
// Utils is the interface that wraps chart-related methods
//
// LookupChartDir looks up the chart's root directory based on some chart file that has changed
type Utils interface {
LookupChartDir(chartDirs []string, dir string) (string, error)
}
// AccountValidator is the interface that wraps Git account validation
//
// Validate checks if account is valid on repoDomain
type AccountValidator interface {
Validate(repoDomain string, account string) error
}
// Chart represents a Helm chart, and can be initialized with the NewChart method.
type Chart struct {
path string
yaml *util.ChartYaml
ciValuesPaths []string
}
// Yaml returns the Chart metadata
func (c *Chart) Yaml() *util.ChartYaml {
return c.yaml
}
// Path returns the chart's directory path
func (c *Chart) Path() string {
return c.path
}
func (c *Chart) String() string {
return fmt.Sprintf(`%s => (version: "%s", path: "%s")`, c.yaml.Name, c.yaml.Version, c.Path())
}
// ValuesFilePathsForCI returns all file paths in the 'ci' subfolder of the chart directory matching the pattern '*-values.yaml'
func (c *Chart) ValuesFilePathsForCI() []string {
return c.ciValuesPaths
}
// HasCIValuesFile checks whether a given CI values file is present.
func (c *Chart) HasCIValuesFile(path string) bool {
fileName := filepath.Base(path)
for _, file := range c.ValuesFilePathsForCI() {
if fileName == filepath.Base(file) {
return true
}
}
return false
}
// CreateInstallParams generates a randomized release name and namespace based on the chart path
// and optional buildID. If a buildID is specified, it will be part of the generated namespace.
func (c *Chart) CreateInstallParams(buildID string) (release string, namespace string) {
release = filepath.Base(c.Path())
if release == "." || release == "/" {
yaml := c.Yaml()
release = yaml.Name
}
namespace = release
if buildID != "" {
namespace = fmt.Sprintf("%s-%s", namespace, buildID)
}
randomSuffix := util.RandomString(10)
release = util.SanitizeName(fmt.Sprintf("%s-%s", release, randomSuffix), maxNameLength)
namespace = util.SanitizeName(fmt.Sprintf("%s-%s", namespace, randomSuffix), maxNameLength)
return
}
// NewChart parses the path to a chart directory and allocates a new Chart object. If chartPath is
// not a valid chart directory an error is returned.
func NewChart(chartPath string) (*Chart, error) {
yaml, err := util.ReadChartYaml(chartPath)
if err != nil {
return nil, err
}
matches, _ := filepath.Glob(filepath.Join(chartPath, "ci", "*-values.yaml"))
return &Chart{chartPath, yaml, matches}, nil
}
type Testing struct {
config config.Configuration
helm Helm
kubectl Kubectl
git Git
linter Linter
cmdExecutor CmdExecutor
accountValidator AccountValidator
directoryLister DirectoryLister
utils Utils
previousRevisionWorktree string
}
// TestResults holds results and overall status
type TestResults struct {
OverallSuccess bool
TestResults []TestResult
}
// TestResult holds test results for a specific chart
type TestResult struct {
Chart *Chart
Error error
}
// NewTesting creates a new Testing struct with the given config.
func NewTesting(config config.Configuration, extraSetArgs string) (Testing, error) {
procExec := exec.NewProcessExecutor(config.Debug)
helmExtraArgs := strings.Fields(config.HelmExtraArgs)
helmLintExtraArgs := strings.Fields(config.HelmLintExtraArgs)
testing := Testing{
config: config,
helm: tool.NewHelm(procExec, helmExtraArgs, helmLintExtraArgs, strings.Fields(extraSetArgs)),
git: tool.NewGit(procExec),
kubectl: tool.NewKubectl(procExec, config.KubectlTimeout),
linter: tool.NewLinter(procExec),
cmdExecutor: tool.NewCmdTemplateExecutor(procExec),
accountValidator: tool.AccountValidator{},
directoryLister: util.DirectoryLister{},
utils: util.Utils{},
}
versionString, err := testing.helm.Version()
if err != nil {
return testing, err
}
version, err := semver.NewVersion(versionString)
if err != nil {
return testing, err
}
if version.Major() < 3 {
return testing, fmt.Errorf("minimum required Helm version is v3.0.0; found: %s", version)
}
return testing, nil
}
// computePreviousRevisionPath converts any file or directory path to the same path in the
// previous revision's working tree.
func (t *Testing) computePreviousRevisionPath(fileOrDirPath string) string {
return filepath.Join(t.previousRevisionWorktree, fileOrDirPath)
}
func (t *Testing) processCharts(action func(chart *Chart) TestResult) ([]TestResult, error) {
var results []TestResult // nolint: prealloc
chartDirs, err := t.FindChartDirsToBeProcessed()
if err != nil {
return nil, fmt.Errorf("failed identifying charts to process: %w", err)
} else if len(chartDirs) == 0 {
return results, nil
}
var charts []*Chart
for _, dir := range chartDirs {
chart, err := NewChart(dir)
if err != nil {
return nil, err
}
if t.config.ExcludeDeprecated && chart.yaml.Deprecated {
fmt.Printf("Chart %q is deprecated and will be ignored because '--exclude-deprecated' is set\n", chart.String())
} else {
charts = append(charts, chart)
}
}
if !t.config.GithubGroups {
fmt.Println()
util.PrintDelimiterLineToWriter(os.Stdout, "-")
fmt.Println(" Charts to be processed:")
util.PrintDelimiterLineToWriter(os.Stdout, "-")
} else {
util.GithubGroupsBegin(os.Stdout, "Charts to be processed")
}
for _, chart := range charts {
fmt.Printf(" %s\n", chart)
}
if !t.config.GithubGroups {
util.PrintDelimiterLineToWriter(os.Stdout, "-")
fmt.Println()
} else {
util.GithubGroupsEnd(os.Stdout)
}
repoArgs := map[string][]string{}
for _, repo := range t.config.HelmRepoExtraArgs {
repoSlice := strings.SplitN(repo, "=", 2)
name := repoSlice[0]
repoExtraArgs := strings.Fields(repoSlice[1])
repoArgs[name] = repoExtraArgs
}
for _, repo := range t.config.ChartRepos {
repoSlice := strings.SplitN(repo, "=", 2)
name := repoSlice[0]
url := repoSlice[1]
repoExtraArgs := repoArgs[name]
if err := t.helm.AddRepo(name, url, repoExtraArgs); err != nil {
return nil, fmt.Errorf("failed adding repo: %s=%s: %w", name, url, err)
}
}
testResults := TestResults{
OverallSuccess: true,
TestResults: results,
}
// Checkout previous chart revisions and build their dependencies
if t.config.Upgrade {
mergeBase, err := t.computeMergeBase()
if err != nil {
return results, fmt.Errorf("failed identifying merge base: %w", err)
}
// Add worktree for the target revision
worktreePath, err := os.MkdirTemp("./", "ct-previous-revision")
if err != nil {
return results, fmt.Errorf("could not create previous revision directory: %w", err)
}
t.previousRevisionWorktree = worktreePath
err = t.git.AddWorktree(worktreePath, mergeBase)
if err != nil {
return results, fmt.Errorf("could not create worktree for previous revision: %w", err)
}
defer t.git.RemoveWorktree(worktreePath) // nolint: errcheck
for _, chart := range charts {
if err := t.helm.BuildDependenciesWithArgs(t.computePreviousRevisionPath(chart.Path()), t.config.HelmDependencyExtraArgs); err != nil {
// Only print error (don't exit) if building dependencies for previous revision fails.
fmt.Printf("failed building dependencies for previous revision of chart %q: %v\n", chart, err.Error())
}
}
}
for _, chart := range charts {
if err := t.helm.BuildDependenciesWithArgs(chart.Path(), t.config.HelmDependencyExtraArgs); err != nil {
return nil, fmt.Errorf("failed building dependencies for chart %q: %w", chart, err)
}
result := action(chart)
if result.Error != nil {
testResults.OverallSuccess = false
}
results = append(results, result)
}
if testResults.OverallSuccess {
return results, nil
}
return results, fmt.Errorf("failed processing charts")
}
// LintCharts lints charts (changed, all, specific) depending on the configuration.
func (t *Testing) LintCharts() ([]TestResult, error) {
return t.processCharts(t.LintChart)
}
// InstallCharts install charts (changed, all, specific) depending on the configuration.
func (t *Testing) InstallCharts() ([]TestResult, error) {
return t.processCharts(t.InstallChart)
}
// LintAndInstallCharts first lints and then installs charts (changed, all, specific) depending on the configuration.
func (t *Testing) LintAndInstallCharts() ([]TestResult, error) {
return t.processCharts(t.LintAndInstallChart)
}
// PrintResults writes test results to stdout.
func (t *Testing) PrintResults(results []TestResult) {
if !t.config.GithubGroups {
fmt.Println()
util.PrintDelimiterLineToWriter(os.Stdout, "-")
} else {
util.GithubGroupsBegin(os.Stdout, "Test Results")
}
if results != nil {
for _, result := range results {
err := result.Error
if err != nil {
fmt.Printf(" %s %s > %s\n", "✖︎", result.Chart, err)
} else {
fmt.Printf(" %s %s\n", "✔︎", result.Chart)
}
}
} else {
fmt.Println("No chart changes detected.")
}
if !t.config.GithubGroups {
util.PrintDelimiterLineToWriter(os.Stdout, "-")
} else {
util.GithubGroupsEnd(os.Stdout)
}
}
// LintChart lints the specified chart.
func (t *Testing) LintChart(chart *Chart) TestResult {
fmt.Printf("Linting chart %q\n", chart)
result := TestResult{Chart: chart}
if t.config.CheckVersionIncrement {
if err := t.CheckVersionIncrement(chart); err != nil {
result.Error = err
return result
}
}
chartYaml := filepath.Join(chart.Path(), "Chart.yaml")
valuesYaml := filepath.Join(chart.Path(), "values.yaml")
valuesFiles := chart.ValuesFilePathsForCI()
if t.config.ValidateChartSchema {
if err := t.linter.Yamale(chartYaml, t.config.ChartYamlSchema); err != nil {
result.Error = err
return result
}
}
if t.config.ValidateYaml {
yamlFiles := append([]string{chartYaml, valuesYaml}, valuesFiles...)
for _, yamlFile := range yamlFiles {
if err := t.linter.YamlLint(yamlFile, t.config.LintConf); err != nil {
result.Error = err
return result
}
}
}
if t.config.ValidateMaintainers {
if err := t.ValidateMaintainers(chart); err != nil {
result.Error = err
return result
}
}
for _, cmd := range t.config.AdditionalCommands {
if err := t.cmdExecutor.RunCommand(cmd, chart); err != nil {
result.Error = err
return result
}
}
// Lint with defaults if no values files are specified.
if len(valuesFiles) == 0 {
valuesFiles = append(valuesFiles, "")
}
for _, valuesFile := range valuesFiles {
if valuesFile != "" {
fmt.Printf("\nLinting chart with values file %q...\n\n", valuesFile)
}
if err := t.helm.LintWithValues(chart.Path(), valuesFile); err != nil {
result.Error = err
break
}
}
return result
}
// InstallChart installs the specified chart into a new namespace, waits for resources to become ready, and eventually
// uninstalls it and deletes the namespace again.
func (t *Testing) InstallChart(chart *Chart) TestResult {
var result TestResult
if t.config.Upgrade {
// Test upgrade from previous version
result = t.UpgradeChart(chart)
if result.Error != nil {
return result
}
// Test upgrade of current version (related: https://github.com/helm/chart-testing/issues/19)
if err := t.doUpgrade(chart, chart, true); err != nil {
result.Error = err
return result
}
}
result = TestResult{Chart: chart}
if err := t.doInstall(chart); err != nil {
result.Error = err
}
return result
}
// UpgradeChart tests in-place upgrades of the specified chart relative to its previous revisions. If the
// initial install or helm test of a previous revision of the chart fails, that release is ignored and no
// error will be returned. If the latest revision of the chart introduces a potentially breaking change
// according to the SemVer specification, upgrade testing will be skipped.
func (t *Testing) UpgradeChart(chart *Chart) TestResult {
result := TestResult{Chart: chart}
breakingChangeAllowed, err := t.checkBreakingChangeAllowed(chart)
if breakingChangeAllowed {
if err != nil {
fmt.Printf("Skipping upgrade test of %q because: %v\n", chart, err.Error())
}
return result
} else if err != nil {
fmt.Printf("Error comparing chart versions for %q\n", chart)
result.Error = err
return result
}
if oldChart, err := NewChart(t.computePreviousRevisionPath(chart.Path())); err == nil {
result.Error = t.doUpgrade(oldChart, chart, false)
}
return result
}
func (t *Testing) doInstall(chart *Chart) error {
fmt.Printf("Installing chart %q...\n", chart)
valuesFiles := chart.ValuesFilePathsForCI()
// Test with defaults if no values files are specified.
if len(valuesFiles) == 0 {
valuesFiles = append(valuesFiles, "")
}
for _, valuesFile := range valuesFiles {
if valuesFile != "" {
fmt.Printf("\nInstalling chart with values file %q...\n\n", valuesFile)
}
// Use anonymous function. Otherwise deferred calls would pile up
// and be executed in reverse order after the loop.
fun := func() error {
namespace, release, releaseSelector, cleanup := t.generateInstallConfig(chart)
if !t.config.SkipCleanUp {
defer cleanup()
}
if t.config.Namespace == "" {
if err := t.kubectl.CreateNamespace(namespace); err != nil {
return err
}
}
if err := t.helm.InstallWithValues(chart.Path(), valuesFile, namespace, release); err != nil {
return err
}
return t.testRelease(namespace, release, releaseSelector)
}
if err := fun(); err != nil {
return err
}
}
return nil
}
func (t *Testing) doUpgrade(oldChart, newChart *Chart, oldChartMustPass bool) error {
fmt.Printf("Testing upgrades of chart %q relative to previous revision %q...\n", newChart, oldChart)
valuesFiles := oldChart.ValuesFilePathsForCI()
if len(valuesFiles) == 0 {
valuesFiles = append(valuesFiles, "")
}
for _, valuesFile := range valuesFiles {
if valuesFile != "" {
if t.config.SkipMissingValues && !newChart.HasCIValuesFile(valuesFile) {
fmt.Printf("Upgrade testing for values file %q skipped because a corresponding values file was not found in %s/ci\n", valuesFile, newChart.Path())
continue
}
fmt.Printf("\nInstalling chart %q with values file %q...\n\n", oldChart, valuesFile)
}
// Use anonymous function. Otherwise deferred calls would pile up
// and be executed in reverse order after the loop.
fun := func() error {
namespace, release, releaseSelector, cleanup := t.generateInstallConfig(oldChart)
if !t.config.SkipCleanUp {
defer cleanup()
}
if t.config.Namespace == "" {
if err := t.kubectl.CreateNamespace(namespace); err != nil {
return err
}
}
// Install previous version of chart. If installation fails, ignore this release.
if err := t.helm.InstallWithValues(oldChart.Path(), valuesFile, namespace, release); err != nil {
if oldChartMustPass {
return err
}
fmt.Printf("Upgrade testing for release %q skipped because of previous revision installation error: %v\n", release, err.Error())
return nil
}
if err := t.testRelease(namespace, release, releaseSelector); err != nil {
if oldChartMustPass {
return err
}
fmt.Printf("Upgrade testing for release %q skipped because of previous revision testing error: %v\n", release, err.Error())
return nil
}
if err := t.helm.Upgrade(newChart.Path(), namespace, release); err != nil {
return err
}
return t.testRelease(namespace, release, releaseSelector)
}
if err := fun(); err != nil {
return err
}
}
return nil
}
func (t *Testing) testRelease(namespace, release, releaseSelector string) error {
if err := t.kubectl.WaitForDeployments(namespace, releaseSelector); err != nil {
return err
}
return t.helm.Test(namespace, release)
}
func (t *Testing) generateInstallConfig(chart *Chart) (namespace, release, releaseSelector string, cleanup func()) {
if t.config.Namespace != "" {
namespace = t.config.Namespace
release, _ = chart.CreateInstallParams(t.config.BuildID)
releaseSelector = fmt.Sprintf("%s=%s", t.config.ReleaseLabel, release)
cleanup = func() {
t.PrintEventsPodDetailsAndLogs(namespace, releaseSelector)
t.helm.DeleteRelease(namespace, release)
}
} else {
release, namespace = chart.CreateInstallParams(t.config.BuildID)
cleanup = func() {
t.PrintEventsPodDetailsAndLogs(namespace, releaseSelector)
t.helm.DeleteRelease(namespace, release)
t.kubectl.DeleteNamespace(namespace)
}
}
return
}
// LintAndInstallChart first lints and then installs the specified chart.
func (t *Testing) LintAndInstallChart(chart *Chart) TestResult {
result := t.LintChart(chart)
if result.Error != nil {
return result
}
return t.InstallChart(chart)
}
// FindChartDirsToBeProcessed identifies charts to be processed depending on the configuration
// (changed charts, all charts, or specific charts).
func (t *Testing) FindChartDirsToBeProcessed() ([]string, error) {
cfg := t.config
if cfg.ProcessAllCharts {
return t.ReadAllChartDirectories()
} else if len(cfg.Charts) > 0 {
return t.config.Charts, nil
}
return t.ComputeChangedChartDirectories()
}
func (t *Testing) computeMergeBase() (string, error) {
err := t.git.ValidateRepository()
if err != nil {
return "", errors.New("must be in a git repository")
}
branch := fmt.Sprintf("%s/%s", t.config.Remote, t.config.TargetBranch)
if !t.git.BranchExists(branch) {
return "", fmt.Errorf("targetBranch '%s' does not exist", branch)
}
return t.git.MergeBase(branch, t.config.Since)
}
// ComputeChangedChartDirectories takes the merge base of HEAD and the configured remote and target branch and computes a
// slice of changed charts from that in the configured chart directories excluding those configured to be excluded.
func (t *Testing) ComputeChangedChartDirectories() ([]string, error) {
cfg := t.config
mergeBase, err := t.computeMergeBase()
if err != nil {
return nil, err
}
allChangedChartFiles, err := t.git.ListChangedFilesInDirs(mergeBase, cfg.ChartDirs...)
if err != nil {
return nil, fmt.Errorf("failed creating diff: %w", err)
}
var changedChartDirs []string
for _, file := range allChangedChartFiles {
pathElements := strings.SplitN(filepath.ToSlash(file), "/", 3)
if len(pathElements) < 2 || util.StringSliceContains(cfg.ExcludedCharts, pathElements[1]) {
continue
}
dir := filepath.Dir(file)
// Make sure directory is really a chart directory
chartDir, err := t.utils.LookupChartDir(cfg.ChartDirs, dir)
chartDirElement := strings.Split(chartDir, "/")
if err == nil {
if len(chartDirElement) > 1 {
chartDirName := chartDirElement[len(chartDirElement)-1]
if util.StringSliceContains(cfg.ExcludedCharts, chartDirName) {
continue
}
}
// Only add it if not already in the list
if !util.StringSliceContains(changedChartDirs, chartDir) {
changedChartDirs = append(changedChartDirs, chartDir)
}
} else {
fmt.Fprintf(os.Stderr, "Directory %q is not a valid chart directory. Skipping...\n", dir)
}
}
return changedChartDirs, nil
}
// ReadAllChartDirectories returns a slice of all charts in the configured chart directories except those
// configured to be excluded.
func (t *Testing) ReadAllChartDirectories() ([]string, error) {
cfg := t.config
var chartDirs []string
for _, chartParentDir := range cfg.ChartDirs {
dirs, err := t.directoryLister.ListChildDirs(chartParentDir,
func(dir string) bool {
_, err := t.utils.LookupChartDir(cfg.ChartDirs, dir)
return err == nil && !util.StringSliceContains(cfg.ExcludedCharts, filepath.Base(dir))
})
if err != nil {
return nil, fmt.Errorf("failed reading chart directories: %w", err)
}
chartDirs = append(chartDirs, dirs...)
}
return chartDirs, nil
}
// CheckVersionIncrement checks that the new chart version is greater than the old one using semantic version comparison.
func (t *Testing) CheckVersionIncrement(chart *Chart) error {
fmt.Printf("Checking chart %q for a version bump...\n", chart)
oldVersion, err := t.GetOldChartVersion(chart.Path())
if err != nil {
return err
}
if oldVersion == "" {
// new chart, skip version check
return nil
}
fmt.Println("Old chart version:", oldVersion)
chartYaml := chart.Yaml()
newVersion := chartYaml.Version
fmt.Println("New chart version:", newVersion)
result, err := util.CompareVersions(oldVersion, newVersion)
if err != nil {
return err
}
if result >= 0 {
return errors.New("chart version not ok. Needs a version bump! ")
}
fmt.Println("Chart version ok.")
return nil
}
func (t *Testing) checkBreakingChangeAllowed(chart *Chart) (allowed bool, err error) {
oldVersion, err := t.GetOldChartVersion(chart.Path())
if err != nil {
return false, err
}
if oldVersion == "" {
// new chart, skip upgrade check
return true, fmt.Errorf("chart has no previous revision")
}
newVersion := chart.Yaml().Version
return util.BreakingChangeAllowed(oldVersion, newVersion)
}
// GetOldChartVersion gets the version of the old Chart.yaml file from the target branch.
func (t *Testing) GetOldChartVersion(chartPath string) (string, error) {
cfg := t.config
chartYamlFile := filepath.Join(chartPath, "Chart.yaml")
if !t.git.FileExistsOnBranch(chartYamlFile, cfg.Remote, cfg.TargetBranch) {
fmt.Printf("Unable to find chart on %s. New chart detected.\n", cfg.TargetBranch)
return "", nil
}
chartYamlContents, err := t.git.Show(chartYamlFile, cfg.Remote, cfg.TargetBranch)
if err != nil {
return "", fmt.Errorf("failed reading old Chart.yaml: %w", err)
}
chartYaml, err := util.UnmarshalChartYaml([]byte(chartYamlContents))
if err != nil {
return "", fmt.Errorf("failed reading old chart version: %w", err)
}
return chartYaml.Version, nil
}
// ValidateMaintainers validates maintainers in the Chart.yaml file. Maintainer names must be valid accounts
// (GitHub, Bitbucket, GitLab) names. Deprecated charts must not have maintainers.
func (t *Testing) ValidateMaintainers(chart *Chart) error {
fmt.Println("Validating maintainers...")
chartYaml := chart.Yaml()
if chartYaml.Deprecated {
if len(chartYaml.Maintainers) > 0 {
return errors.New("deprecated chart must not have maintainers")
}
return nil
}
if len(chartYaml.Maintainers) == 0 {
return errors.New("chart doesn't have maintainers")
}
repoURL, err := t.git.GetURLForRemote(t.config.Remote)
if err != nil {
return err
}
for _, maintainer := range chartYaml.Maintainers {
if err := t.accountValidator.Validate(repoURL, maintainer.Name); err != nil {
return err
}
}
return nil
}
func (t *Testing) PrintEventsPodDetailsAndLogs(namespace string, selector string) {
util.PrintDelimiterLineToWriter(os.Stdout, "=")
t.printDetails(namespace, "Events of namespace", ".", func(item string) error {
return t.kubectl.GetEvents(namespace)
}, namespace)
pods, err := t.kubectl.GetPods(
"--no-headers",
"--namespace",
namespace,
"--selector",
selector,
"--output",
"jsonpath={.items[*].metadata.name}",
)
if err != nil {
fmt.Println("Error printing logs:", err)
return
}
for _, pod := range pods {
t.printDetails(pod, "Description of pod", "~", func(item string) error {
return t.kubectl.DescribePod(namespace, pod)
}, pod)
initContainers, err := t.kubectl.GetInitContainers(namespace, pod)
if err != nil {
fmt.Println("Error printing logs:", err)
return
}
if t.config.PrintLogs {
t.printDetails(pod, "Logs of init container", "-",
func(item string) error {
return t.kubectl.Logs(namespace, pod, item)
}, initContainers...)
containers, err := t.kubectl.GetContainers(namespace, pod)
if err != nil {
fmt.Printf("failed printing logs: %v\n", err.Error())
return
}
t.printDetails(pod, "Logs of container", "-",
func(item string) error {
return t.kubectl.Logs(namespace, pod, item)
},
containers...)
}
}
util.PrintDelimiterLineToWriter(os.Stdout, "=")
}
func (t *Testing) printDetails(resource string, text string, delimiterChar string, printFunc func(item string) error, items ...string) {
for _, item := range items {
item = strings.Trim(item, "'")
if !t.config.GithubGroups {
util.PrintDelimiterLineToWriter(os.Stdout, delimiterChar)
fmt.Printf("==> %s %s\n", text, resource)
util.PrintDelimiterLineToWriter(os.Stdout, delimiterChar)
} else {
util.GithubGroupsBegin(os.Stdout, fmt.Sprintf("%s %s", text, resource))
}
if err := printFunc(item); err != nil {
fmt.Println("Error printing details:", err)
return
}
if !t.config.GithubGroups {
util.PrintDelimiterLineToWriter(os.Stdout, delimiterChar)
fmt.Printf("<== %s %s\n", text, resource)
util.PrintDelimiterLineToWriter(os.Stdout, delimiterChar)
} else {
util.GithubGroupsEnd(os.Stdout)
}
}
}