/
validate.go
202 lines (186 loc) · 8.67 KB
/
validate.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
package validate
import (
"fmt"
"path/filepath"
"strings"
"github.com/go-git/go-billy/v5"
"github.com/rancher/charts-build-scripts/pkg/filesystem"
"github.com/rancher/charts-build-scripts/pkg/options"
"github.com/rancher/charts-build-scripts/pkg/path"
"github.com/rancher/charts-build-scripts/pkg/puller"
"github.com/rancher/charts-build-scripts/pkg/standardize"
"github.com/rancher/charts-build-scripts/pkg/zip"
"github.com/sirupsen/logrus"
helmLoader "helm.sh/helm/v3/pkg/chart/loader"
)
const ReleaseYamlFileName = "release.yaml"
// CompareGeneratedAssetsResponse tracks resources that are added, deleted, and modified when comparing two charts repositories
type CompareGeneratedAssetsResponse struct {
// UntrackedInRelease represents charts that need to be added to the release.yaml
UntrackedInRelease options.ReleaseOptions `yaml:"untrackedInRelease,omitempty"`
// RemovedPostRelease represents charts that have been removed from the upstream
RemovedPostRelease options.ReleaseOptions `yaml:"removedPostRelease,omitempty"`
// ModifiedPostRelease represents charts that have been modified from the upstream
ModifiedPostRelease options.ReleaseOptions `yaml:"modifiedPostRelease,omitempty"`
}
// PassedValidation returns whether the response seems to indicate that the chart repositories are in sync
func (r CompareGeneratedAssetsResponse) PassedValidation() bool {
return len(r.UntrackedInRelease) == 0 && len(r.RemovedPostRelease) == 0 && len(r.ModifiedPostRelease) == 0
}
// LogDiscrepancies produces logs that can be used to pretty-print why a validation might have failed
func (r CompareGeneratedAssetsResponse) LogDiscrepancies() {
logrus.Errorf("The following new assets have been introduced: %s", r.UntrackedInRelease)
logrus.Errorf("The following released assets have been removed: %s", r.RemovedPostRelease)
logrus.Errorf("The following released assets have been modified: %s", r.ModifiedPostRelease)
logrus.Errorf("If this was intentional, to allow validation to pass, these charts must be added to the release.yaml.")
}
// DumpReleaseYaml takes the response collected by this CompareGeneratedAssetsResponse and automatically creates the appropriate release.yaml,
// assuming that the user does indeed intend to add, delete, or modify all assets that were marked in this comparison
func (r CompareGeneratedAssetsResponse) DumpReleaseYaml(repoFs billy.Filesystem) error {
releaseYaml, err := options.LoadReleaseOptionsFromFile(repoFs, ReleaseYamlFileName)
if err != nil {
return err
}
if releaseYaml == nil {
releaseYaml = make(map[string][]string)
}
releaseYaml.Merge(r.UntrackedInRelease)
releaseYaml.Merge(r.RemovedPostRelease)
releaseYaml.Merge(r.ModifiedPostRelease)
return releaseYaml.WriteToFile(repoFs, ReleaseYamlFileName)
}
// CompareGeneratedAssets checks to see if current assets and charts match upstream, aside from those indicated in the release.yaml
// It returns a boolean indicating if the comparison has passed or an error
func CompareGeneratedAssets(repoFs billy.Filesystem, u options.UpstreamOptions, branch string, releaseOptions options.ReleaseOptions) (CompareGeneratedAssetsResponse, error) {
response := CompareGeneratedAssetsResponse{
UntrackedInRelease: options.ReleaseOptions{},
ModifiedPostRelease: options.ReleaseOptions{},
RemovedPostRelease: options.ReleaseOptions{},
}
// Pull repository
logrus.Infof("Pulling upstream repository %s at branch %s", u.URL, branch)
releasedChartsRepoBranch, err := puller.GetGithubRepository(u, &branch)
if err != nil {
return response, fmt.Errorf("failed to get Github repository pointing to new upstream: %s", err)
}
if err := releasedChartsRepoBranch.Pull(repoFs, repoFs, path.ChartsRepositoryUpstreamBranchDir); err != nil {
return response, fmt.Errorf("failed to pull assets from upstream: %s", err)
}
defer filesystem.RemoveAll(repoFs, path.ChartsRepositoryUpstreamBranchDir)
// Standardize the upstream repository
logrus.Infof("Standardizing upstream repository to compare it against local")
releaseFs, err := repoFs.Chroot(path.ChartsRepositoryUpstreamBranchDir)
if err != nil {
return response, fmt.Errorf("failed to get filesystem for %s: %s", path.ChartsRepositoryUpstreamBranchDir, err)
}
if err := standardize.RestructureChartsAndAssets(releaseFs); err != nil {
return response, fmt.Errorf("failed to standardize upstream: %s", err)
}
// Walk through directories and execute release logic
localOnly := func(fs billy.Filesystem, localPath string, isDir bool) error {
if isDir {
// We only care about original files
return nil
}
isAsset := strings.HasPrefix(localPath, path.RepositoryAssetsDir+"/")
hasTgzExtension := filepath.Ext(localPath) == ".tgz"
if !isAsset || !hasTgzExtension {
// We only care about assets
return nil
}
// Check if the chart is tracked in release
chart, err := helmLoader.Load(filesystem.GetAbsPath(fs, localPath))
if err != nil {
return err
}
if releaseOptions.Contains(chart.Metadata.Name, chart.Metadata.Version) {
// Chart is tracked in release.yaml
return nil
}
// Chart exists in local and is not tracked by release.yaml
logrus.Infof("%s/%s is untracked", chart.Metadata.Name, chart.Metadata.Version)
response.UntrackedInRelease = response.UntrackedInRelease.Append(chart.Metadata.Name, chart.Metadata.Version)
return nil
}
upstreamOnly := func(fs billy.Filesystem, upstreamPath string, isDir bool) error {
if isDir {
// We only care about original files
return nil
}
isAsset := strings.HasPrefix(upstreamPath, filepath.Join(path.ChartsRepositoryUpstreamBranchDir, path.RepositoryAssetsDir)+"/")
hasTgzExtension := filepath.Ext(upstreamPath) == ".tgz"
if !isAsset || !hasTgzExtension {
// We only care about assets
return nil
}
// Check if the chart is tracked in release
chart, err := helmLoader.Load(filesystem.GetAbsPath(fs, upstreamPath))
if err != nil {
return err
}
if releaseOptions.Contains(chart.Metadata.Name, chart.Metadata.Version) {
// Chart is tracked in release.yaml; this chart was removed intentionally
return nil
}
// Chart was removed from local and is not tracked by release.yaml
logrus.Infof("%s/%s was removed", chart.Metadata.Name, chart.Metadata.Version)
response.RemovedPostRelease = response.RemovedPostRelease.Append(chart.Metadata.Name, chart.Metadata.Version)
// Found asset that only exists in upstream and is not tracked by release.yaml
localPath, err := filesystem.MovePath(upstreamPath, path.ChartsRepositoryUpstreamBranchDir, "")
if err != nil {
return err
}
return copyAndUnzip(repoFs, upstreamPath, localPath)
}
localAndUpstream := func(fs billy.Filesystem, localPath, upstreamPath string, isDir bool) error {
if isDir {
// We only care about modified files
return nil
}
isAsset := strings.HasPrefix(localPath, path.RepositoryAssetsDir+"/")
hasTgzExtension := filepath.Ext(localPath) == ".tgz"
if !isAsset || !hasTgzExtension {
// We only care about assets
return nil
}
// Check if the chart is tracked in release
chart, err := helmLoader.Load(filesystem.GetAbsPath(fs, upstreamPath))
if err != nil {
return err
}
if releaseOptions.Contains(chart.Metadata.Name, chart.Metadata.Version) {
// Chart is tracked in release.yaml
return nil
}
// Deep compare the inner contents of the tgzs
identical, err := filesystem.CompareTgzs(fs, upstreamPath, localPath)
if err != nil {
return err
}
if identical {
return nil
}
// Chart was modified in local and is not tracked by release.yaml
logrus.Infof("%s/%s was modified", chart.Metadata.Name, chart.Metadata.Version)
response.ModifiedPostRelease = response.ModifiedPostRelease.Append(chart.Metadata.Name, chart.Metadata.Version)
return copyAndUnzip(repoFs, upstreamPath, localPath)
}
logrus.Infof("Comparing standardized upstream assets against local assets")
if err := filesystem.CompareDirs(repoFs, "", path.ChartsRepositoryUpstreamBranchDir, localOnly, upstreamOnly, localAndUpstream); err != nil {
return response, fmt.Errorf("encountered error while trying to compare local against upstream: %s", err)
}
return response, nil
}
func copyAndUnzip(repoFs billy.Filesystem, upstreamPath, localPath string) error {
specificAsset, err := filesystem.MovePath(upstreamPath, filepath.Join(path.ChartsRepositoryUpstreamBranchDir, path.RepositoryAssetsDir), "")
if err != nil {
return fmt.Errorf("encountered error while trying to find repository path for upstream path %s: %s", upstreamPath, err)
}
if err := filesystem.CopyFile(repoFs, upstreamPath, localPath); err != nil {
return err
}
if err := zip.DumpAssets(repoFs.Root(), specificAsset); err != nil {
return fmt.Errorf("encountered error while copying over contents of modified upstream asset to charts: %s", err)
}
return nil
}