integrate clustertool sourcecode

This commit is contained in:
Kjeld Schouten
2024-10-16 14:06:31 +02:00
parent bc2a642e7a
commit 6365c6205f
265 changed files with 24073 additions and 45 deletions
@@ -0,0 +1,227 @@
package chartFile
import (
"bytes"
"fmt"
"os"
"github.com/go-playground/validator/v10"
"github.com/truecharts/private/clustertool/pkg/helper"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
)
const (
minHelmVersion = "3.11"
maxHelmVersion = "3.15"
kubeVersion = ">=1.24.0-0"
apiVersion = "v2"
chartType = "application"
maintainerName = "TrueCharts"
maintainerEmail = "info@truecharts.org"
maintainerURL = "https://truecharts.org"
defaultCategory = "unsorted"
defaultAppVersion = "unknown"
defaultDescription = "No description provided."
defaultHome = "https://truecharts.org"
defaultIcon = "https://github.com/truecharts/website/blob/main/static/svg/logo.svg"
)
var validate *validator.Validate
// Maintainer represents a maintainer of the Helm chart.
type Maintainer struct {
Name string `yaml:"name" validate:"required"`
Email string `yaml:"email"`
URL string `yaml:"url" validate:"required"`
}
// Dependency represents a dependency of the Helm chart.
type Dependency struct {
Name string `yaml:"name" validate:"required"`
Version string `yaml:"version" validate:"required"`
Repository string `yaml:"repository" validate:"required"`
Condition string `yaml:"condition"`
Alias string `yaml:"alias"`
Tags []string `yaml:"tags"`
ImportValues []string `yaml:"import-values"`
}
// ChartMetadata represents the metadata structure in Chart.yaml.
type ChartMetadata struct {
Annotations map[string]string `yaml:"annotations"`
APIVersion string `yaml:"apiVersion" validate:"required"`
AppVersion string `yaml:"appVersion" validate:"required"`
Dependencies []Dependency `yaml:"dependencies"`
Deprecated bool `yaml:"deprecated"`
Description string `yaml:"description" validate:"required"`
Home string `yaml:"home" validate:"required"`
Icon string `yaml:"icon" validate:"required"`
Keywords []string `yaml:"keywords"`
KubeVersion string `yaml:"kubeVersion" validate:"required"`
Maintainers []Maintainer `yaml:"maintainers" validate:"required,dive"`
Name string `yaml:"name" validate:"required"`
Sources []string `yaml:"sources"`
Type string `yaml:"type"`
Version string `yaml:"version" validate:"required"`
// Add other fields as needed
}
// HelmChart represents the entire Chart.yaml structure.
type HelmChart struct {
K *koanf.Koanf
Metadata ChartMetadata `yaml:"metadata" validate:"required,dive"`
// Add other fields as needed
}
func NewHelmChart() *HelmChart {
return &HelmChart{
K: koanf.New("."),
}
}
// LoadFromFile loads values from a YAML file into the HelmChart struct.
func (h *HelmChart) LoadFromFile(filename string) error {
// Load YAML file using koanf
if err := h.K.Load(file.Provider(filename), yaml.Parser()); err != nil {
return fmt.Errorf("error loading from file %s: %v", filename, err)
}
// Unmarshal the data into the HelmChart struct
if err := h.K.Unmarshal("", &h.Metadata); err != nil {
return fmt.Errorf("error unmarshalling data: %v", err)
}
// Set default values for fields if they are not set or empty
h.setDefaultValues()
// Initialize validator
validate = validator.New(validator.WithRequiredStructEnabled())
if err := validate.Struct(h.Metadata); err != nil {
return fmt.Errorf("chart.yaml validation error: %v", err)
}
return nil
}
// setDefaultValues sets default values for fields in ChartMetadata if they are not set or empty.
func (h *HelmChart) setDefaultValues() {
h.setDeprecation()
h.setApiVersion(apiVersion)
h.setKubeVersion(kubeVersion)
h.setType(chartType)
h.setAppVersion(defaultAppVersion)
h.setDescription(defaultDescription)
h.setIcon(defaultIcon)
h.setHome(defaultHome)
h.setMaintainers(Maintainer{
Name: maintainerName,
Email: maintainerEmail,
URL: maintainerURL,
})
// Make sure annotations is not nil
if h.Metadata.Annotations == nil {
h.Metadata.Annotations = make(map[string]string)
}
h.setAnnotation("truecharts.org/category", defaultCategory, false)
h.setAnnotation("truecharts.org/min_helm_version", minHelmVersion, true)
h.setAnnotation("truecharts.org/max_helm_version", maxHelmVersion, true)
// Set default values for other fields as needed
}
// SaveToFile saves the Helm chart metadata back to the Chart.yaml file.
func (h *HelmChart) SaveToFile(filename string) error {
// Initialize validator
validate = validator.New(validator.WithRequiredStructEnabled())
if err := validate.Struct(h.Metadata); err != nil {
return fmt.Errorf("chart.yaml validation error: %v", err)
}
var configBytes bytes.Buffer
err := helper.MarshalYaml(&configBytes, h.Metadata)
if err != nil {
return fmt.Errorf("error encoding data: %v", err)
}
// Write the configuration to the file using os.WriteFile
err = os.WriteFile(filename, configBytes.Bytes(), 0644)
if err != nil {
return fmt.Errorf("error writing to file %s: %v", filename, err)
}
return nil
}
// setAnnotation sets the annotation key to value if it is not set or empty or force is true.
func (h *HelmChart) setAnnotation(key, value string, force bool) {
if a, ok := h.Metadata.Annotations[key]; !ok || a == "" || force {
h.Metadata.Annotations[key] = value
}
}
// setDeprecation sets the deprecation field to false if it is not set.
func (h *HelmChart) setDeprecation() {
if !h.Metadata.Deprecated {
h.Metadata.Deprecated = false
}
}
// setIcon sets the icon field to icon if it is not set or empty.
func (h *HelmChart) setIcon(icon string) {
if h.Metadata.Icon == "" {
h.Metadata.Icon = icon
}
}
// setHome sets the home field to home if it is not set or empty.
func (h *HelmChart) setHome(home string) {
if h.Metadata.Home == "" {
h.Metadata.Home = home
}
}
// setDescription sets the description field to description if it is not set or empty.
func (h *HelmChart) setDescription(description string) {
if h.Metadata.Description == "" {
h.Metadata.Description = description
}
}
// setAppVersion sets the appVersion field to appVersion if it is not set or empty.
func (h *HelmChart) setAppVersion(appVersion string) {
if h.Metadata.AppVersion == "" {
h.Metadata.AppVersion = appVersion
}
}
// setType sets the type field to cType if it is not set or empty.
func (h *HelmChart) setType(cType string) {
if h.Metadata.Type == "" {
h.Metadata.Type = cType
}
}
// setApiVersion sets the apiVersion field to apiVersion
func (h *HelmChart) setApiVersion(apiVersion string) {
h.Metadata.APIVersion = apiVersion
}
// setKubeVersion sets the kubeVersion field to kubeVersion
func (h *HelmChart) setKubeVersion(kubeVersion string) {
h.Metadata.KubeVersion = kubeVersion
}
// setMaintainers sets the maintainers field to maintainers
func (h *HelmChart) setMaintainers(maintainers Maintainer) {
h.Metadata.Maintainers = make([]Maintainer, 1)
h.Metadata.Maintainers[0] = maintainers
}
@@ -0,0 +1,621 @@
package chartFile
import (
"fmt"
"reflect"
"testing"
)
func TestSetAnnotation(t *testing.T) {
type args struct {
key string
value string
force bool
}
type testData struct {
name string
data args
initial map[string]string
want map[string]string
}
tests := []testData{
{
name: "Should set annotation when not present",
initial: map[string]string{},
want: map[string]string{
"test": "test",
},
data: args{
key: "test",
value: "test",
force: false,
},
},
{
name: "Should not set annotation when present and force is false",
initial: map[string]string{
"test": "value",
},
want: map[string]string{
"test": "value",
},
data: args{
key: "test",
value: "test",
force: false,
},
},
{
name: "Should set annotation when present and force is true",
initial: map[string]string{
"test": "value",
},
want: map[string]string{
"test": "test",
},
data: args{
key: "test",
value: "test",
force: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewHelmChart()
h.Metadata.Annotations = tt.initial
h.setAnnotation(tt.data.key, tt.data.value, tt.data.force)
if !reflect.DeepEqual(h.Metadata.Annotations, tt.want) {
t.Errorf("%s - Annotations, got %v, want %v", tt.name, h.Metadata.Annotations, tt.want)
}
})
}
}
func TestSetDeprecation(t *testing.T) {
type testData struct {
name string
input ChartMetadata
want ChartMetadata
}
tests := []testData{
{
name: "Should set deprecation to false when not present",
input: ChartMetadata{
Deprecated: false,
},
want: ChartMetadata{
Deprecated: false,
},
},
{
name: "Should not set deprecation when present",
input: ChartMetadata{
Deprecated: true,
},
want: ChartMetadata{
Deprecated: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewHelmChart()
h.Metadata = tt.input
h.setDeprecation()
if !reflect.DeepEqual(h.Metadata, tt.want) {
t.Errorf("%s - Metadata, got %v, want %v", tt.name, h.Metadata, tt.want)
}
})
}
}
type TestData struct {
name string
value string
initial ChartMetadata
want ChartMetadata
}
func TestSetIcon(t *testing.T) {
tests := []TestData{
{
name: "Should set icon when not present",
value: "test",
initial: ChartMetadata{
Icon: "",
},
want: ChartMetadata{
Icon: "test",
},
},
{
name: "Should not set icon when present",
value: "test",
initial: ChartMetadata{
Icon: "some-icon",
},
want: ChartMetadata{
Icon: "some-icon",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewHelmChart()
h.Metadata = tt.initial
h.setIcon(tt.value)
if !reflect.DeepEqual(h.Metadata, tt.want) {
t.Errorf("%s - Metadata, got %v, want %v", tt.name, h.Metadata, tt.want)
}
})
}
}
func TestSetHome(t *testing.T) {
tests := []TestData{
{
name: "Should set home when not present",
value: "test",
initial: ChartMetadata{
Home: "",
},
want: ChartMetadata{
Home: "test",
},
},
{
name: "Should not set home when present",
value: "test",
initial: ChartMetadata{
Home: "some-home",
},
want: ChartMetadata{
Home: "some-home",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewHelmChart()
h.Metadata = tt.initial
h.setHome(tt.value)
if !reflect.DeepEqual(h.Metadata, tt.want) {
t.Errorf("%s - Metadata, got %v, want %v", tt.name, h.Metadata, tt.want)
}
})
}
}
func TestSetDescription(t *testing.T) {
tests := []TestData{
{
name: "Should set description when not present",
value: "test",
initial: ChartMetadata{
Description: "",
},
want: ChartMetadata{
Description: "test",
},
},
{
name: "Should not set description when present",
value: "test",
initial: ChartMetadata{
Description: "some-description",
},
want: ChartMetadata{
Description: "some-description",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewHelmChart()
h.Metadata = tt.initial
h.setDescription(tt.value)
if !reflect.DeepEqual(h.Metadata, tt.want) {
t.Errorf("%s - Metadata, got %v, want %v", tt.name, h.Metadata, tt.want)
}
})
}
}
func TestSetAppVersion(t *testing.T) {
tests := []TestData{
{
name: "Should set appVersion when not present",
value: "test",
initial: ChartMetadata{
AppVersion: "",
},
want: ChartMetadata{
AppVersion: "test",
},
},
{
name: "Should not set appVersion when present",
value: "test",
initial: ChartMetadata{
AppVersion: "some-appVersion",
},
want: ChartMetadata{
AppVersion: "some-appVersion",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewHelmChart()
h.Metadata = tt.initial
h.setAppVersion(tt.value)
if !reflect.DeepEqual(h.Metadata, tt.want) {
t.Errorf("%s - Metadata, got %v, want %v", tt.name, h.Metadata, tt.want)
}
})
}
}
func TestSetType(t *testing.T) {
tests := []TestData{
{
name: "Should set type when not present",
value: "test",
initial: ChartMetadata{
Type: "",
},
want: ChartMetadata{
Type: "test",
},
},
{
name: "Should not set type when present",
value: "test",
initial: ChartMetadata{
Type: "some-type",
},
want: ChartMetadata{
Type: "some-type",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewHelmChart()
h.Metadata = tt.initial
h.setType(tt.value)
if !reflect.DeepEqual(h.Metadata, tt.want) {
t.Errorf("%s - Metadata, got %v, want %v", tt.name, h.Metadata, tt.want)
}
})
}
}
func TestSetApiVersion(t *testing.T) {
tests := []TestData{
{
name: "Should set apiVersion when not present",
value: "test",
initial: ChartMetadata{
APIVersion: "",
},
want: ChartMetadata{
APIVersion: "test",
},
},
{
name: "Should always set apiVersion when present",
value: "test",
initial: ChartMetadata{
APIVersion: "some-apiVersion",
},
want: ChartMetadata{
APIVersion: "test",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewHelmChart()
h.Metadata = tt.initial
h.setApiVersion(tt.value)
if !reflect.DeepEqual(h.Metadata, tt.want) {
t.Errorf("%s - Metadata, got %v, want %v", tt.name, h.Metadata, tt.want)
}
})
}
}
func TestSetKubeVersion(t *testing.T) {
tests := []TestData{
{
name: "Should set kubeVersion when not present",
value: "test",
initial: ChartMetadata{
KubeVersion: "",
},
want: ChartMetadata{
KubeVersion: "test",
},
},
{
name: "Should always set kubeVersion when present",
value: "test",
initial: ChartMetadata{
KubeVersion: "some-kubeVersion",
},
want: ChartMetadata{
KubeVersion: "test",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewHelmChart()
h.Metadata = tt.initial
h.setKubeVersion(tt.value)
if !reflect.DeepEqual(h.Metadata, tt.want) {
t.Errorf("%s - Metadata, got %v, want %v", tt.name, h.Metadata, tt.want)
}
})
}
}
func TestSetMaintainers(t *testing.T) {
type testData struct {
name string
value Maintainer
initial ChartMetadata
want ChartMetadata
}
tests := []testData{
{
name: "Should set maintainers when not present",
value: Maintainer{
Name: "test-name",
Email: "test-mail",
URL: "test-url",
},
initial: ChartMetadata{
Maintainers: []Maintainer{},
},
want: ChartMetadata{
Maintainers: []Maintainer{
{
Name: "test-name",
Email: "test-mail",
URL: "test-url",
},
},
},
},
{
name: "Should always set maintainers when present",
value: Maintainer{
Name: "test-name",
Email: "test-mail",
URL: "test-url",
},
initial: ChartMetadata{
Maintainers: []Maintainer{
{
Name: "some-maintainer",
Email: "some-mail",
URL: "some-url",
},
},
},
want: ChartMetadata{
Maintainers: []Maintainer{
{
Name: "test-name",
Email: "test-mail",
URL: "test-url",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewHelmChart()
h.Metadata = tt.initial
h.setMaintainers(tt.value)
if !reflect.DeepEqual(h.Metadata.Maintainers, tt.want.Maintainers) {
t.Errorf("%s - Maintainers, got %v, want %v", tt.name, h.Metadata.Maintainers, tt.want.Maintainers)
}
})
}
}
func TestSetDefaults(t *testing.T) {
type testData struct {
name string
initial ChartMetadata
want ChartMetadata
}
tests := []testData{
{
name: "Should set defaults",
initial: ChartMetadata{},
want: ChartMetadata{
KubeVersion: kubeVersion,
APIVersion: apiVersion,
Type: chartType,
Deprecated: false,
AppVersion: defaultAppVersion,
Description: defaultDescription,
Home: defaultHome,
Icon: defaultIcon,
Maintainers: []Maintainer{
{
Name: maintainerName,
Email: maintainerEmail,
URL: maintainerURL,
},
},
Annotations: map[string]string{
"truecharts.org/category": defaultCategory,
"truecharts.org/min_helm_version": minHelmVersion,
"truecharts.org/max_helm_version": maxHelmVersion,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewHelmChart()
h.Metadata = tt.initial
h.setDefaultValues()
if !reflect.DeepEqual(h.Metadata, tt.want) {
t.Errorf("%s - Metadata, got %v, want %v", tt.name, h.Metadata, tt.want)
}
})
}
}
func TestLoadFromFile(t *testing.T) {
type testData struct {
name string
file string
wantErr bool
}
testDataPath := "../../testdata/chart_yaml"
tests := []testData{
{
name: "Should load from file",
file: "validChart.yaml",
wantErr: false,
},
{
name: "Should fail to load from malformed file",
file: "malformedChart.yaml",
wantErr: true,
},
{
name: "Should fail to load from missing file",
file: "missingChart.yaml",
wantErr: true,
},
{
name: "Should fail to load from invalid file",
file: "invalidChart.yaml",
wantErr: true,
},
{
name: "Should fail to load from unmashalable file",
file: "unmarshalableChart.yaml",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewHelmChart()
err := h.LoadFromFile(fmt.Sprintf("%s/%s", testDataPath, tt.file))
if (err != nil) != tt.wantErr {
t.Errorf("%s - LoadFromFile() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
})
}
}
func TestSaveToFile(t *testing.T) {
type testData struct {
name string
inFile string
outFile string
mutatedData ChartMetadata
shouldMutate bool
wantErr bool
}
testDataPath := "../../testdata/chart_yaml"
tests := []testData{
{
name: "Should fail to save to file",
inFile: "validChart.yaml",
outFile: "/tmp/test.yaml",
mutatedData: ChartMetadata{},
shouldMutate: true,
wantErr: true,
},
{
name: "Should save to file",
inFile: "validChart.yaml",
outFile: "/tmp/test.yaml",
mutatedData: ChartMetadata{},
shouldMutate: false,
wantErr: false,
},
{
name: "Should fail to write to file",
inFile: "validChart.yaml",
outFile: "/non-existent-dir/test.yaml",
mutatedData: ChartMetadata{},
shouldMutate: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewHelmChart()
if err := h.LoadFromFile(fmt.Sprintf("%s/%s", testDataPath, tt.inFile)); err != nil {
t.Errorf("%s - LoadFromFile() error = %v", tt.name, err)
}
if tt.shouldMutate {
h.Metadata = tt.mutatedData
}
err := h.SaveToFile(tt.outFile)
if (err != nil) != tt.wantErr {
t.Errorf("%s - SaveToFile() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
})
}
}
+208
View File
@@ -0,0 +1,208 @@
package chartFile
import (
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"github.com/rs/zerolog/log"
"github.com/truecharts/private/clustertool/pkg/charts/helmignore"
"github.com/truecharts/private/clustertool/pkg/charts/image"
"github.com/truecharts/private/clustertool/pkg/charts/readme"
"github.com/truecharts/private/clustertool/pkg/charts/version"
)
// UpdateChartFile updates the specified Chart.yaml file with an optional bump parameter.
func UpdateChartFile(chartPathOrFolder, bump string) error {
fileInfo, err := os.Stat(chartPathOrFolder)
if err != nil {
return err
}
chartPath := chartPathOrFolder
if fileInfo.IsDir() {
chartPath = filepath.Join(chartPathOrFolder, "Chart.yaml")
}
log.Info().Msgf("🏃 Processing chart [%s]", chartPath)
chart := NewHelmChart()
if err := chart.LoadFromFile(chartPath); err != nil {
return err
}
if chart.Metadata.Annotations == nil {
chart.Metadata.Annotations = make(map[string]string)
}
train := GetTrain(chartPath, chart)
setMetadata(chart, train)
var values image.Images
// Fetch image details from values.yaml
if err := values.LoadValuesFile(filepath.Join(filepath.Dir(chartPath), "values.yaml")); err != nil {
return err
}
setAppVersionFromImage(chart, &values, "image")
var imageLinks []string
for _, details := range values.ImagesMap {
imageLinks = append(imageLinks, details.Link)
}
// Attempt to update sources
if err := updateSources(chart, train, imageLinks); err != nil {
return err
}
// Update appVersion, icon, and home URLs
if bump == version.Major || bump == version.Minor || bump == version.Patch {
newVersion, err := version.IncrementVersion(chart.Metadata.Version, bump)
log.Info().Msgf("🆚 Bumping [%s], from [%s] to [%s]", chart.Metadata.Name, chart.Metadata.Version, newVersion)
if err != nil {
log.Error().Err(err).Msg("Error bumping version")
}
chart.Metadata.Version = newVersion
}
// Save the modified metadata back to the file
if err := chart.SaveToFile(chartPath); err != nil {
return fmt.Errorf("error saving Chart.yaml: %s", err)
}
log.Info().Msgf("Chart file updated and saved to [%s]", chartPath)
templateDir := chartPath
for i := 0; i < 4; i++ {
templateDir = filepath.Dir(templateDir)
}
// Generate README.md for the specified train and chart
readmeErr := readme.GenerateReadme(templateDir, chartPath, chart.Metadata.Name, train)
if readmeErr != nil {
log.Info().Msgf("Error Generating readme for %v: %v\n", chart.Metadata.Name, readmeErr)
os.Exit(1)
}
// Generate .helmignore for the specified train and chart
helmignoreErr := helmignore.GenerateHelmIgnore(templateDir, chartPath)
if helmignoreErr != nil {
log.Info().Msgf("Error Generating helmignore for %v: %v\n", chart.Metadata.Name, helmignoreErr)
os.Exit(1)
}
return nil
}
func setAppVersionFromImage(chart *HelmChart, imageMap *image.Images, key string) {
imageDetails, exists := imageMap.ImagesMap[key]
if !exists {
log.Warn().Msgf("Details for image key [%s] not found in values.yaml, skipping setting appVersion", key)
return
}
log.Info().Msgf("Detected - Tag [%s], Image Version [%s]", imageDetails.Tag, imageDetails.Version)
chart.Metadata.AppVersion = imageDetails.Version
}
// detectTrainFromFile detects the train name based on the path of Chart.yaml.
func detectTrainFromFile(chartFilename string) string {
parts := strings.Split(
// Remove the filename from the path
filepath.Dir(chartFilename),
string(os.PathSeparator),
)
if len(parts) >= 2 {
return parts[len(parts)-2]
}
// One case to reach here is when the tool is run from inside the train directory
// But we can't safely assume the current directory is the train directory
log.Error().Msgf("Unable to detect train from path [%s]", chartFilename)
return ""
}
func GetTrain(chartPath string, chart *HelmChart) string {
// Detect the train from the path of Chart.yaml
// Do not rely on the annotations in the chart, as they may be outdated
train := detectTrainFromFile(chartPath)
if train == "" {
// If the train cannot be detected from the path, fallback to detect it from the annotations
if val, exists := chart.Metadata.Annotations["truecharts.org/train"]; exists {
train = val
} else {
log.Error().Msgf("Unable to detect train for chart [%s]. Setting as [unknown]", chart.Metadata.Name)
train = "unknown"
}
}
return train
}
func setMetadata(chart *HelmChart, train string) {
chart.Metadata.Annotations["truecharts.org/train"] = train
chart.Metadata.Icon = fmt.Sprintf("https://truecharts.org/img/hotlink-ok/chart-icons/%s.webp", chart.Metadata.Name)
chart.Metadata.Home = fmt.Sprintf("https://truecharts.org/charts/%s/%s", train, chart.Metadata.Name)
}
// UpdateSources updates the sources in Chart.yaml using Go.
func updateSources(chart *HelmChart, train string, imageLinks []string) error {
var updatedSources []string
// Those sources are automatically generated by this tool,
// So we only need to keep sources that are not in this list
for _, source := range chart.Metadata.Sources {
if !strings.HasPrefix(source, "https://ghcr") &&
!strings.HasPrefix(source, "https://docker.io") &&
!strings.HasPrefix(source, "https://hub.docker") &&
!strings.HasPrefix(source, "https://fleet.linuxserver") &&
!strings.HasPrefix(source, "https://mcr.microsoft") &&
!strings.HasPrefix(source, "https://cr.hotio.dev") &&
!strings.HasPrefix(source, "https://github.com/truecharts") &&
!strings.HasPrefix(source, "https://gallery.ecr.aws") &&
!strings.HasPrefix(source, "https://gcr") &&
!strings.HasPrefix(source, "https://quay") &&
!strings.HasPrefix(source, "http://") &&
!strings.Contains(source, ".azurecr.io") &&
!strings.Contains(source, ".ocir.io") {
if source != "" {
log.Info().Msgf("🔗 Keeping source [%s]", source)
updatedSources = append(updatedSources, source)
}
}
}
// Add the GitHub source for the chart
ghSource := fmt.Sprintf("https://github.com/truecharts/charts/tree/master/charts/%s/%s", train, chart.Metadata.Name)
updatedSources = append(updatedSources, ghSource)
// Add new sources for each image
updatedSources = append(updatedSources, imageLinks...)
// Deduplicate sources
deduplicatedSources := make(map[string]bool)
var finalSources []string
for _, source := range updatedSources {
// Skip empty sources
if source == "" {
continue
}
// Skip sources that have already been added
if _, exists := deduplicatedSources[source]; exists {
continue
}
// Add the source to the list of sources and mark it as added
deduplicatedSources[source] = true
finalSources = append(finalSources, source)
}
// Sort the sources, so subsequent commits will only include actual changes
slices.Sort(finalSources)
// Update the chart's sources
chart.Metadata.Sources = finalSources
return nil
}
@@ -0,0 +1,230 @@
package chartFile
import (
"reflect"
"testing"
"github.com/truecharts/private/clustertool/pkg/charts/image"
)
func TestSetAppVersionFromImage(t *testing.T) {
type TestData struct {
chart *HelmChart
image *image.Images
key string
result string
}
tests := []TestData{
{
chart: &HelmChart{
Metadata: ChartMetadata{
AppVersion: "1.0.0",
},
},
image: &image.Images{
ImagesMap: map[string]image.ImageDetails{
"image": {
Repository: "nginx",
Tag: "1.15.8",
Link: "https://hub.docker.com/_/nginx",
Version: "1.15.8",
},
},
},
key: "image",
result: "1.15.8",
},
{
chart: &HelmChart{
Metadata: ChartMetadata{
AppVersion: "1.0.0",
},
},
image: &image.Images{
ImagesMap: map[string]image.ImageDetails{
"image": {
Repository: "nginx",
Tag: "1.15.8",
Link: "https://hub.docker.com/_/nginx",
Version: "1.15.8",
},
},
},
key: "nonexistent",
result: "1.0.0",
},
}
for _, tt := range tests {
setAppVersionFromImage(tt.chart, tt.image, tt.key)
if tt.chart.Metadata.AppVersion != tt.result {
t.Errorf("Expected %s, got %s", tt.result, tt.chart.Metadata.AppVersion)
}
}
}
func TestGetTrain(t *testing.T) {
type TestData struct {
name string
chart *HelmChart
chartPath string
result string
}
tests := []TestData{
{
name: "Test get train from path",
chart: &HelmChart{
Metadata: ChartMetadata{
Name: "test-chart",
Annotations: map[string]string{
"truecharts.org/train": "express",
},
},
},
chartPath: "../../testdata/updater/stable/my-app/Chart.yaml",
result: "stable",
},
{
name: "Test get train from annotations as fallback",
chart: &HelmChart{
Metadata: ChartMetadata{
Name: "test-chart",
Annotations: map[string]string{
"truecharts.org/train": "dev",
},
},
},
// Too short path, cant detect train from path
// so we should fallback to annotations
chartPath: "my-app/Chart.yaml",
result: "dev",
},
{
name: "Test failing to get train from path or annotations",
chart: &HelmChart{
Metadata: ChartMetadata{
Name: "test-chart",
Annotations: map[string]string{},
},
},
// Too short path, cant detect train from path
// so we should fallback to annotations
chartPath: "my-app/Chart.yaml",
result: "unknown",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
train := GetTrain(tt.chartPath, tt.chart)
if train != tt.result {
t.Errorf("Expected train to be %s, but got %s", tt.result, train)
}
})
}
}
func TestSetMetadata(t *testing.T) {
type TestData struct {
chart *HelmChart
train string
expected *HelmChart
}
tests := []TestData{
{
chart: &HelmChart{
Metadata: ChartMetadata{
Name: "test-chart",
Annotations: map[string]string{},
},
},
train: "stable",
expected: &HelmChart{
Metadata: ChartMetadata{
Name: "test-chart",
Annotations: map[string]string{
"truecharts.org/train": "stable",
},
Icon: "https://truecharts.org/img/hotlink-ok/chart-icons/test-chart.webp",
Home: "https://truecharts.org/charts/stable/test-chart",
},
},
},
}
for _, tt := range tests {
t.Run(tt.chart.Metadata.Name, func(t *testing.T) {
setMetadata(tt.chart, tt.train)
if !reflect.DeepEqual(tt.chart, tt.expected) {
t.Errorf("Expected chart to be %v, but got %v", tt.expected, tt.chart)
}
})
}
}
func TestUpdateSources(t *testing.T) {
type TestData struct {
name string
chart *HelmChart
train string
imageLinks []string
expected []string
}
tests := []TestData{
{
name: "Test update sources",
chart: &HelmChart{
Metadata: ChartMetadata{
Name: "test-chart",
Sources: []string{
"",
"https://ghcr/truecharts/some-chart",
"https://docker.io/truecharts/some-chart",
"https://hub.docker/truecharts/some-chart",
"https://fleet.linuxserver/truecharts/some-chart",
"https://mcr.microsoft/truecharts/some-chart",
"https://github.com/truecharts/some-chart",
"https://gallery.ecr.aws/truecharts/some-chart",
"https://gcr/truecharts/some-chart",
"https://quay/truecharts/some-chart",
"http://truecharts/some-chart",
"https://truecharts.azurecr.io/some-chart",
"https://truecharts.ocir.io/some-chart",
"https://unrelated.com/some-chart",
"https://unrelated.com/some-chart",
"https://cr.hotio.dev/truecharts/some-chart",
},
},
},
train: "stable",
imageLinks: []string{
"",
"https://hub.docker.com/_/nginx",
"https://quay.io/truecharts/test-chart",
},
expected: []string{
"https://github.com/truecharts/charts/tree/master/charts/stable/test-chart",
"https://hub.docker.com/_/nginx",
"https://quay.io/truecharts/test-chart",
"https://unrelated.com/some-chart",
},
},
}
for _, tt := range tests {
t.Run(tt.chart.Metadata.Name, func(t *testing.T) {
if err := updateSources(tt.chart, tt.train, tt.imageLinks); err != nil {
t.Errorf("Expected no error, but got %v", err)
}
if !reflect.DeepEqual(tt.chart.Metadata.Sources, tt.expected) {
t.Errorf("Expected chart to be %v, but got %v", tt.expected, tt.chart.Metadata.Sources)
}
})
}
}