fix pre-commit and cleanup

This commit is contained in:
Kjeld Schouten
2024-10-19 15:18:05 +02:00
parent 2acb59c9dc
commit 4ab41d42f2
203 changed files with 8456 additions and 8484 deletions
@@ -1,60 +1,60 @@
package changelog
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog/log"
)
type ActiveChart struct {
Name string
Train string
Name string
Train string
}
type ActiveCharts struct {
items map[string]ActiveChart
mu *sync.RWMutex
items map[string]ActiveChart
mu *sync.RWMutex
}
func (a *ActiveCharts) isActiveChart(chartName string) bool {
a.mu.RLock()
defer a.mu.RUnlock()
_, ok := a.items[chartName]
return ok
a.mu.RLock()
defer a.mu.RUnlock()
_, ok := a.items[chartName]
return ok
}
func (a *ActiveCharts) getActiveChartsWalker(path string, entry os.DirEntry, err error) error {
if err != nil {
return err
}
if entry.Name() != "Chart.yaml" {
return nil
}
// path = charts/<train>/<chart>/Chart.yaml
segLen := len(strings.Split(path, "/"))
if segLen < 3 {
return fmt.Errorf("path (%s) is not valid. expected at least charts/<train>/<chart>/Chart.yaml", path)
}
// chart = charts/<train>/<chart>/
chart, _ := filepath.Split(path)
// chart = charts/<train>/<chart>
chart = strings.TrimSuffix(chart, "/")
// train = charts/<train>
train := filepath.Dir(chart)
// train = <train>
train = filepath.Base(train)
// chartName = <chart>
chartName := filepath.Base(chart)
a.mu.Lock()
if _, ok := a.items[chartName]; !ok {
a.items[chartName] = ActiveChart{Name: chartName, Train: train}
} else {
log.Error().Msgf("chart [%s] already exists in activeCharts", chartName)
}
a.mu.Unlock()
return nil
if err != nil {
return err
}
if entry.Name() != "Chart.yaml" {
return nil
}
// path = charts/<train>/<chart>/Chart.yaml
segLen := len(strings.Split(path, "/"))
if segLen < 3 {
return fmt.Errorf("path (%s) is not valid. expected at least charts/<train>/<chart>/Chart.yaml", path)
}
// chart = charts/<train>/<chart>/
chart, _ := filepath.Split(path)
// chart = charts/<train>/<chart>
chart = strings.TrimSuffix(chart, "/")
// train = charts/<train>
train := filepath.Dir(chart)
// train = <train>
train = filepath.Base(train)
// chartName = <chart>
chartName := filepath.Base(chart)
a.mu.Lock()
if _, ok := a.items[chartName]; !ok {
a.items[chartName] = ActiveChart{Name: chartName, Train: train}
} else {
log.Error().Msgf("chart [%s] already exists in activeCharts", chartName)
}
a.mu.Unlock()
return nil
}
+136 -136
View File
@@ -1,187 +1,187 @@
package changelog
import (
"encoding/json"
"errors"
"fmt"
"os"
"sort"
"sync"
"time"
"encoding/json"
"errors"
"fmt"
"os"
"sort"
"sync"
"time"
"github.com/Masterminds/semver/v3"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/rs/zerolog/log"
"github.com/Masterminds/semver/v3"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/rs/zerolog/log"
)
type ChangedData struct {
mu *sync.RWMutex `json:"-"`
LastCommit string `json:"last_commit"`
Charts map[string]*Chart `json:"charts"`
mu *sync.RWMutex `json:"-"`
LastCommit string `json:"last_commit"`
Charts map[string]*Chart `json:"charts"`
}
type Chart struct {
Versions map[string]*Version `json:"versions"`
SortedVersions []string `json:"-"` // Used only for rendering
Name string `json:"-"` // Used only for rendering
Train string `json:"-"` // Used only for rendering
Versions map[string]*Version `json:"versions"`
SortedVersions []string `json:"-"` // Used only for rendering
Name string `json:"-"` // Used only for rendering
Train string `json:"-"` // Used only for rendering
}
func (c *Chart) SortVersions(reverse bool) ([]*semver.Version, error) {
chartVersions := []*semver.Version{}
for key := range c.Versions {
semVer, err := semver.NewVersion(key)
if err != nil {
return nil, err
}
chartVersions = append(chartVersions, semVer)
}
// Sort the versions from oldest to newest
sort.Slice(chartVersions, func(i, j int) bool {
if reverse {
return chartVersions[i].GreaterThan(chartVersions[j])
}
return chartVersions[i].LessThan(chartVersions[j])
})
chartVersions := []*semver.Version{}
for key := range c.Versions {
semVer, err := semver.NewVersion(key)
if err != nil {
return nil, err
}
chartVersions = append(chartVersions, semVer)
}
// Sort the versions from oldest to newest
sort.Slice(chartVersions, func(i, j int) bool {
if reverse {
return chartVersions[i].GreaterThan(chartVersions[j])
}
return chartVersions[i].LessThan(chartVersions[j])
})
for _, version := range chartVersions {
c.SortedVersions = append(c.SortedVersions, version.String())
}
for _, version := range chartVersions {
c.SortedVersions = append(c.SortedVersions, version.String())
}
return chartVersions, nil
return chartVersions, nil
}
func (c *ChangedData) AddOrUpdateChart(chart string, version string, train string, commit *object.Commit) {
if c.Charts == nil {
c.Charts = make(map[string]*Chart)
}
_, exists := c.Charts[chart]
if !exists {
c.Charts[chart] = &Chart{}
}
if c.Charts == nil {
c.Charts = make(map[string]*Chart)
}
_, exists := c.Charts[chart]
if !exists {
c.Charts[chart] = &Chart{}
}
c.Charts[chart].AddVersion(version, train)
c.Charts[chart].Versions[version].AddCommit(commit)
c.Charts[chart].AddVersion(version, train)
c.Charts[chart].Versions[version].AddCommit(commit)
}
func (c *Chart) AddVersion(version string, train string) {
if c.Versions == nil {
c.Versions = make(map[string]*Version)
}
_, exists := c.Versions[version]
if exists {
return
}
c.Versions[version] = &Version{
Version: version,
Train: train,
Commits: make(map[string]*Commit),
}
if c.Versions == nil {
c.Versions = make(map[string]*Version)
}
_, exists := c.Versions[version]
if exists {
return
}
c.Versions[version] = &Version{
Version: version,
Train: train,
Commits: make(map[string]*Commit),
}
}
type Version struct {
Version string `json:"version"`
Train string `json:"train"`
Commits map[string]*Commit `json:"commits"`
SortedCommits []*Commit `json:"-"` // Used only for rendering
Version string `json:"version"`
Train string `json:"train"`
Commits map[string]*Commit `json:"commits"`
SortedCommits []*Commit `json:"-"` // Used only for rendering
}
func (v *Version) AddCommit(commit *object.Commit) {
if v.Commits == nil {
v.Commits = make(map[string]*Commit)
}
if v.Commits == nil {
v.Commits = make(map[string]*Commit)
}
_, exists := v.Commits[commit.Hash.String()]
if exists {
return
}
v.Commits[commit.Hash.String()] = &Commit{
CommitHash: commit.Hash.String(),
ParentHash: commit.ParentHashes[0].String(),
Author: Author{Name: commit.Author.Name, Date: commit.Author.When.Format(dateFormat)},
Message: getCommitMessage(commit),
Kind: getCommitKind(commit),
}
_, exists := v.Commits[commit.Hash.String()]
if exists {
return
}
v.Commits[commit.Hash.String()] = &Commit{
CommitHash: commit.Hash.String(),
ParentHash: commit.ParentHashes[0].String(),
Author: Author{Name: commit.Author.Name, Date: commit.Author.When.Format(dateFormat)},
Message: getCommitMessage(commit),
Kind: getCommitKind(commit),
}
}
func (v *Version) SortCommits(reverse bool) ([]*Commit, error) {
commits := []*Commit{}
for _, commit := range v.Commits {
commits = append(commits, commit)
}
commits := []*Commit{}
for _, commit := range v.Commits {
commits = append(commits, commit)
}
hasErr := false
sort.Slice(commits, func(i, j int) bool {
// While we could store the time.Time in the Author struct,
// it was giving mixed results as the timezones were different.
// The dateFormat we use does not contain timezone, so it sorts better.
iDate, err := time.Parse(dateFormat, commits[i].Author.Date)
if err != nil {
log.Fatal().Err(err).Msgf("Failed to parse date [%s]", commits[i].Author.Date)
return false
}
jDate, err := time.Parse(dateFormat, commits[j].Author.Date)
if err != nil {
log.Fatal().Err(err).Msgf("Failed to parse date [%s]", commits[j].Author.Date)
return false
}
if reverse {
return iDate.After(jDate)
}
return iDate.Before(jDate)
})
hasErr := false
sort.Slice(commits, func(i, j int) bool {
// While we could store the time.Time in the Author struct,
// it was giving mixed results as the timezones were different.
// The dateFormat we use does not contain timezone, so it sorts better.
iDate, err := time.Parse(dateFormat, commits[i].Author.Date)
if err != nil {
log.Fatal().Err(err).Msgf("Failed to parse date [%s]", commits[i].Author.Date)
return false
}
jDate, err := time.Parse(dateFormat, commits[j].Author.Date)
if err != nil {
log.Fatal().Err(err).Msgf("Failed to parse date [%s]", commits[j].Author.Date)
return false
}
if reverse {
return iDate.After(jDate)
}
return iDate.Before(jDate)
})
if hasErr {
return nil, errors.New("failed to sort commits")
}
if hasErr {
return nil, errors.New("failed to sort commits")
}
v.SortedCommits = commits
return commits, nil
v.SortedCommits = commits
return commits, nil
}
type Commit struct {
CommitHash string `json:"commit_hash"`
ParentHash string `json:"parent_hash"`
Author Author `json:"author"`
Kind string `json:"kind"`
Message string `json:"message"`
CommitHash string `json:"commit_hash"`
ParentHash string `json:"parent_hash"`
Author Author `json:"author"`
Kind string `json:"kind"`
Message string `json:"message"`
}
type Author struct {
Name string `json:"name"`
Date string `json:"date"`
Name string `json:"name"`
Date string `json:"date"`
}
func (c *ChangedData) LoadFromFile(path string) error {
fileInfo, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
if fileInfo.IsDir() {
return fmt.Errorf("path is a directory")
}
fileInfo, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
if fileInfo.IsDir() {
return fmt.Errorf("path is a directory")
}
bytes, err := os.ReadFile(path)
if err != nil {
return err
}
bytes, err := os.ReadFile(path)
if err != nil {
return err
}
err = json.Unmarshal(bytes, &c)
if err != nil {
return err
}
err = json.Unmarshal(bytes, &c)
if err != nil {
return err
}
return nil
return nil
}
func (c *ChangedData) WriteToFile(path string) error {
data, err := json.MarshalIndent(c, "", " ")
if err != nil {
return err
}
log.Info().Msgf("Writing changed data to [%s]", path)
return os.WriteFile(path, data, 0644)
data, err := json.MarshalIndent(c, "", " ")
if err != nil {
return err
}
log.Info().Msgf("Writing changed data to [%s]", path)
return os.WriteFile(path, data, 0644)
}
+206 -206
View File
@@ -1,80 +1,80 @@
package changelog
import (
"fmt"
"os"
"sync"
"time"
"fmt"
"os"
"sync"
"time"
"github.com/Masterminds/semver/v3"
"github.com/go-git/go-git/v5"
"github.com/rs/zerolog/log"
"github.com/truecharts/public/clustertool/pkg/helper"
"github.com/Masterminds/semver/v3"
"github.com/go-git/go-git/v5"
"github.com/rs/zerolog/log"
"github.com/truecharts/public/clustertool/pkg/helper"
)
type ChangelogOptions struct {
RepoPath string // Path to the repository (eg "./charts")
TemplatePath string // Path to the template file (eg "./changelog.tmpl")
ChangelogFileName string // Name of the changelog file eg "CHANGELOG.md"
JSONOutputPath string // Path to the JSON output file
PrettyJSON bool // If true, the JSON output will be pretty-printed
ChartsDir string // Dir where the charts are located (eg "./charts/")
StatusUpdateInterval int // Interval in seconds between status updates
SkipCommitsWithBadMessage bool // If true, commits with bad messages will be skipped
RepoPath string // Path to the repository (eg "./charts")
TemplatePath string // Path to the template file (eg "./changelog.tmpl")
ChangelogFileName string // Name of the changelog file eg "CHANGELOG.md"
JSONOutputPath string // Path to the JSON output file
PrettyJSON bool // If true, the JSON output will be pretty-printed
ChartsDir string // Dir where the charts are located (eg "./charts/")
StatusUpdateInterval int // Interval in seconds between status updates
SkipCommitsWithBadMessage bool // If true, commits with bad messages will be skipped
}
func checkPath(path string, createIfNotExist bool) error {
_, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
if createIfNotExist {
_, err := os.Create(path)
if err != nil {
return fmt.Errorf("cannot create path %s: %w", path, err)
}
return nil
}
return nil
}
return fmt.Errorf("path %s cannot be used: %w", path, err)
}
return nil
_, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
if createIfNotExist {
_, err := os.Create(path)
if err != nil {
return fmt.Errorf("cannot create path %s: %w", path, err)
}
return nil
}
return nil
}
return fmt.Errorf("path %s cannot be used: %w", path, err)
}
return nil
}
func (o *ChangelogOptions) validate() error {
if o.RepoPath == "" {
return fmt.Errorf("repo path is empty")
}
if o.TemplatePath == "" {
return fmt.Errorf("template path is empty")
}
if o.ChangelogFileName == "" {
return fmt.Errorf("changelog file name is empty")
}
if o.ChartsDir == "" {
return fmt.Errorf("charts dir is empty")
}
if o.JSONOutputPath == "" {
return fmt.Errorf("json output path is empty")
}
if o.StatusUpdateInterval <= 0 {
return fmt.Errorf("status update interval is zero")
}
if o.RepoPath == "" {
return fmt.Errorf("repo path is empty")
}
if o.TemplatePath == "" {
return fmt.Errorf("template path is empty")
}
if o.ChangelogFileName == "" {
return fmt.Errorf("changelog file name is empty")
}
if o.ChartsDir == "" {
return fmt.Errorf("charts dir is empty")
}
if o.JSONOutputPath == "" {
return fmt.Errorf("json output path is empty")
}
if o.StatusUpdateInterval <= 0 {
return fmt.Errorf("status update interval is zero")
}
paths := map[string]bool{
o.TemplatePath: false,
o.RepoPath: false,
o.ChartsDir: false,
o.JSONOutputPath: false,
}
paths := map[string]bool{
o.TemplatePath: false,
o.RepoPath: false,
o.ChartsDir: false,
o.JSONOutputPath: false,
}
for path, create := range paths {
if err := checkPath(path, create); err != nil {
return err
}
}
for path, create := range paths {
if err := checkPath(path, create); err != nil {
return err
}
}
return nil
return nil
}
var changedData ChangedData = ChangedData{mu: &sync.RWMutex{}, Charts: make(map[string]*Chart)}
@@ -85,173 +85,173 @@ var skipCommitsWithBadMessage bool
var dateFormat = "2006-01-02"
func (o *ChangelogOptions) Generate() error {
start := time.Now()
skipCommitsWithBadMessage = o.SkipCommitsWithBadMessage
log.Info().Msgf("Starting changelog generation at %s", start)
if err := o.validate(); err != nil {
return err
}
// Get active train and charts
if err := helper.WalkCharts2([]string{o.RepoPath}, activeCharts.getActiveChartsWalker, helper.AsyncMode); err != nil {
return err
}
log.Info().Msgf("Found [%d] active charts in [%s]", len(activeCharts.items), time.Since(start))
start := time.Now()
skipCommitsWithBadMessage = o.SkipCommitsWithBadMessage
log.Info().Msgf("Starting changelog generation at %s", start)
if err := o.validate(); err != nil {
return err
}
// Get active train and charts
if err := helper.WalkCharts2([]string{o.RepoPath}, activeCharts.getActiveChartsWalker, helper.AsyncMode); err != nil {
return err
}
log.Info().Msgf("Found [%d] active charts in [%s]", len(activeCharts.items), time.Since(start))
// Load existing json file
if err := changedData.LoadFromFile(o.JSONOutputPath); err != nil {
return fmt.Errorf("failed to load existing json file, maybe it is not matching the current structure: %w", err)
}
if changedData.LastCommit == "" {
log.Info().Msgf("No last commit found in [%s], starting from the beginning", o.JSONOutputPath)
} else {
log.Info().Msgf("Last commit found in [%s], will start from [%s]", o.JSONOutputPath, changedData.LastCommit)
}
// Load existing json file
if err := changedData.LoadFromFile(o.JSONOutputPath); err != nil {
return fmt.Errorf("failed to load existing json file, maybe it is not matching the current structure: %w", err)
}
if changedData.LastCommit == "" {
log.Info().Msgf("No last commit found in [%s], starting from the beginning", o.JSONOutputPath)
} else {
log.Info().Msgf("Last commit found in [%s], will start from [%s]", o.JSONOutputPath, changedData.LastCommit)
}
// Open repo
repo, err := git.PlainOpen(o.RepoPath)
if err != nil {
return err
}
// Open repo
repo, err := git.PlainOpen(o.RepoPath)
if err != nil {
return err
}
// Get list of commits. Order by committer time and only keep commits that are in the charts dir
// the iterator will yield the newer commits first, so we need to reverse the order
cIter, err := repo.Log(&git.LogOptions{Order: git.LogOrderCommitterTime})
if err != nil {
return err
}
commits, err := o.reverseCommits(cIter, changedData.LastCommit)
if err != nil {
return err
}
if len(commits) == 0 {
log.Info().Msgf("No commits to process in %s", o.RepoPath)
return nil
}
log.Info().Msgf("Found [%d] commits to process in %s", len(commits), o.RepoPath)
// Get list of commits. Order by committer time and only keep commits that are in the charts dir
// the iterator will yield the newer commits first, so we need to reverse the order
cIter, err := repo.Log(&git.LogOptions{Order: git.LogOrderCommitterTime})
if err != nil {
return err
}
commits, err := o.reverseCommits(cIter, changedData.LastCommit)
if err != nil {
return err
}
if len(commits) == 0 {
log.Info().Msgf("No commits to process in %s", o.RepoPath)
return nil
}
log.Info().Msgf("Found [%d] commits to process in %s", len(commits), o.RepoPath)
stop := make(chan struct{}) // Stop channel
defer close(stop)
go o.statusPrinter(stop)
stop := make(chan struct{}) // Stop channel
defer close(stop)
go o.statusPrinter(stop)
// TODO: Once go-git is thread safe, we can parallelize this
// https://github.com/go-git/go-git/issues/773
for _, c := range commits {
changedData.mu.Lock()
changedData.LastCommit = c.Hash.String()
changedData.mu.Unlock()
commitStart := time.Now()
// TODO: Once go-git is thread safe, we can parallelize this
// https://github.com/go-git/go-git/issues/773
for _, c := range commits {
changedData.mu.Lock()
changedData.LastCommit = c.Hash.String()
changedData.mu.Unlock()
commitStart := time.Now()
if err := processCommit(c); err != nil {
log.Error().Err(err).Msgf("Error processing commit: %s", c.Hash.String())
return err
}
if err := processCommit(c); err != nil {
log.Error().Err(err).Msgf("Error processing commit: %s", c.Hash.String())
return err
}
currentStatus.mu.Lock()
currentStatus.processedCount++
currentStatus.totalProcessingTime += time.Since(commitStart)
currentStatus.avgTime = currentStatus.totalProcessingTime / time.Duration(currentStatus.processedCount+currentStatus.skippedCount)
currentStatus.mu.Unlock()
}
currentStatus.mu.Lock()
currentStatus.processedCount++
currentStatus.totalProcessingTime += time.Since(commitStart)
currentStatus.avgTime = currentStatus.totalProcessingTime / time.Duration(currentStatus.processedCount+currentStatus.skippedCount)
currentStatus.mu.Unlock()
}
stop <- struct{}{}
stop <- struct{}{}
if err := mergeStagingToCurrent(); err != nil {
return err
}
if err := changedData.WriteToFile(o.JSONOutputPath); err != nil {
return fmt.Errorf("error writing json new file: %s", err)
}
log.Info().Msgf("Finished in %s", time.Since(start))
o.printStatus(start, false)
return nil
if err := mergeStagingToCurrent(); err != nil {
return err
}
if err := changedData.WriteToFile(o.JSONOutputPath); err != nil {
return fmt.Errorf("error writing json new file: %s", err)
}
log.Info().Msgf("Finished in %s", time.Since(start))
o.printStatus(start, false)
return nil
}
// We have to go over the stagingData, for each chart,
// we sort the versions from the changelogData
// and we add the commits from stagingData to the nearest next version in changelogData
func mergeStagingToCurrent() error {
start := time.Now()
log.Info().Msgf("Merging staging to current", )
changedData.mu.Lock()
defer changedData.mu.Unlock()
start := time.Now()
log.Info().Msgf("Merging staging to current", )
changedData.mu.Lock()
defer changedData.mu.Unlock()
stagingData.mu.Lock()
defer stagingData.mu.Unlock()
for chart, stagingChartItem := range stagingData.Charts {
// If the staging chart doesn't exist in the changelogData, we add it and go to the next chart
chartItem, ok := changedData.Charts[chart]
if !ok {
changedData.Charts[chart] = stagingChartItem
continue
}
stagingData.mu.Lock()
defer stagingData.mu.Unlock()
for chart, stagingChartItem := range stagingData.Charts {
// If the staging chart doesn't exist in the changelogData, we add it and go to the next chart
chartItem, ok := changedData.Charts[chart]
if !ok {
changedData.Charts[chart] = stagingChartItem
continue
}
// If the chart exists in the changelogData but does not have any versions
// we add the versions from stagingData and go to the next chart (probably a new chart)
if chartItem.Versions == nil || len(chartItem.Versions) == 0 {
chartItem.Versions = stagingChartItem.Versions
continue
}
// If the chart exists in the changelogData but does not have any versions
// we add the versions from stagingData and go to the next chart (probably a new chart)
if chartItem.Versions == nil || len(chartItem.Versions) == 0 {
chartItem.Versions = stagingChartItem.Versions
continue
}
// Get all the versions from the changedData chart
chartVersions, err := chartItem.SortVersions(false)
if err != nil {
return err
}
// Get all the versions from the changedData chart
chartVersions, err := chartItem.SortVersions(false)
if err != nil {
return err
}
// Go over the versions in stagingData chart,
// for each version, we find the immediately next version in changedData chart
for versionKey := range stagingData.Charts[chart].Versions {
stagingVer, err := semver.NewVersion(versionKey)
if err != nil { // This should never happen
return err
}
// Go over the versions in stagingData chart,
// for each version, we find the immediately next version in changedData chart
for versionKey := range stagingData.Charts[chart].Versions {
stagingVer, err := semver.NewVersion(versionKey)
if err != nil { // This should never happen
return err
}
foundGreater := false
// Go over the versions in the changedData chart versions
for _, chartVer := range chartVersions {
// If the changedData version is greater than the staging version,
// we add the commits to this version and break
if !chartVer.GreaterThan(stagingVer) {
continue
}
foundGreater = true
chartVerItem, ok := chartItem.Versions[versionKey]
if !ok {
chartItem.AddVersion(versionKey, stagingChartItem.Versions[versionKey].Train)
chartVerItem = chartItem.Versions[versionKey]
}
foundGreater := false
// Go over the versions in the changedData chart versions
for _, chartVer := range chartVersions {
// If the changedData version is greater than the staging version,
// we add the commits to this version and break
if !chartVer.GreaterThan(stagingVer) {
continue
}
foundGreater = true
chartVerItem, ok := chartItem.Versions[versionKey]
if !ok {
chartItem.AddVersion(versionKey, stagingChartItem.Versions[versionKey].Train)
chartVerItem = chartItem.Versions[versionKey]
}
// Add the commits from stagingData to the given version in the changedData chart
for commitKey, commit := range stagingChartItem.Versions[versionKey].Commits {
if chartVerItem.Commits == nil {
log.Warn().Msgf("Commits were nil for version [%s] in chart [%s]", versionKey, chart)
chartVerItem.Commits = make(map[string]*Commit)
}
// Add the commits from stagingData to the given version in the changedData chart
for commitKey, commit := range stagingChartItem.Versions[versionKey].Commits {
if chartVerItem.Commits == nil {
log.Warn().Msgf("Commits were nil for version [%s] in chart [%s]", versionKey, chart)
chartVerItem.Commits = make(map[string]*Commit)
}
if _, ok := chartVerItem.Commits[commitKey]; ok {
// This should never happen, but we log it just in case
log.Warn().Msgf("Commit [%s] already exists in version [%s]", commitKey, versionKey)
continue
}
chartVerItem.Commits[commitKey] = commit
}
break
}
if !foundGreater {
// Add the version to the changedData chart
for commitKey, commit := range stagingChartItem.Versions[versionKey].Commits {
if _, ok := chartItem.Versions[versionKey].Commits[commitKey]; ok {
// This should never happen, but we log it just in case
log.Warn().Msgf("Commit [%s] already exists in version [%s]", commitKey, versionKey)
continue
}
chartItem.Versions[versionKey].Commits[commitKey] = commit
}
}
}
if _, ok := chartVerItem.Commits[commitKey]; ok {
// This should never happen, but we log it just in case
log.Warn().Msgf("Commit [%s] already exists in version [%s]", commitKey, versionKey)
continue
}
chartVerItem.Commits[commitKey] = commit
}
break
}
if !foundGreater {
// Add the version to the changedData chart
for commitKey, commit := range stagingChartItem.Versions[versionKey].Commits {
if _, ok := chartItem.Versions[versionKey].Commits[commitKey]; ok {
// This should never happen, but we log it just in case
log.Warn().Msgf("Commit [%s] already exists in version [%s]", commitKey, versionKey)
continue
}
chartItem.Versions[versionKey].Commits[commitKey] = commit
}
}
}
}
}
log.Info().Msgf("Finished merging in %s", time.Since(start))
return nil
log.Info().Msgf("Finished merging in %s", time.Since(start))
return nil
}
+56 -56
View File
@@ -1,89 +1,89 @@
package changelog
import (
"fmt"
"regexp"
"strings"
"fmt"
"regexp"
"strings"
"github.com/go-git/go-git/v5/plumbing/format/diff"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/rs/zerolog/log"
"github.com/go-git/go-git/v5/plumbing/format/diff"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/rs/zerolog/log"
)
// getCommitMessage returns the first line of the commit message
func getCommitMessage(c *object.Commit) string {
return strings.TrimSpace(strings.Split(c.Message, "\n")[0])
return strings.TrimSpace(strings.Split(c.Message, "\n")[0])
}
// TODO: update regex
var commitMessageRegex = regexp.MustCompile(`^(chore|feat|fix|docs)\((.+)\)?: (.+)`)
func getCommitKind(c *object.Commit) string {
match := commitMessageRegex.FindStringSubmatch(getCommitMessage(c))
if match == nil {
return ""
}
return match[1]
match := commitMessageRegex.FindStringSubmatch(getCommitMessage(c))
if match == nil {
return ""
}
return match[1]
}
func isValidCommit(c *object.Commit) bool {
if c.Message == "" {
currentStatus.incSkippedCount()
log.Debug().Msgf("Skipping commit [%s]. Reason: the commit message is empty", c.Hash.String())
return false
}
if c.ParentHashes == nil || len(c.ParentHashes) == 0 {
currentStatus.incSkippedCount()
log.Debug().Msgf("Skipping commit [%s]. Reason: the commit is Batman (has no parent)", c.Hash.String())
return false
}
if c.Message == "" {
currentStatus.incSkippedCount()
log.Debug().Msgf("Skipping commit [%s]. Reason: the commit message is empty", c.Hash.String())
return false
}
if c.ParentHashes == nil || len(c.ParentHashes) == 0 {
currentStatus.incSkippedCount()
log.Debug().Msgf("Skipping commit [%s]. Reason: the commit is Batman (has no parent)", c.Hash.String())
return false
}
if skipCommitsWithBadMessage && !commitMessageRegex.MatchString(getCommitMessage(c)) {
currentStatus.incSkippedCount()
log.Debug().Msgf("Skipping commit [%s]. Reason: the commit message does not match the pattern", c.Hash.String())
return false
}
if skipCommitsWithBadMessage && !commitMessageRegex.MatchString(getCommitMessage(c)) {
currentStatus.incSkippedCount()
log.Debug().Msgf("Skipping commit [%s]. Reason: the commit message does not match the pattern", c.Hash.String())
return false
}
return true
return true
}
type oldNewPaths struct {
old diff.File
new diff.File
old diff.File
new diff.File
}
type chartsWithChangedFiles map[string][]oldNewPaths
type chartsWithChangedFile map[string]oldNewPaths
func processCommit(c *object.Commit) error {
var err error
if !isValidCommit(c) {
return nil
}
var err error
if !isValidCommit(c) {
return nil
}
parCommit, err := c.Parent(0)
if err != nil {
return fmt.Errorf("failed to get parent commit: %w", err)
}
patch, err := parCommit.Patch(c)
if err != nil {
return fmt.Errorf("failed to get patch: %w", err)
}
parCommit, err := c.Parent(0)
if err != nil {
return fmt.Errorf("failed to get parent commit: %w", err)
}
patch, err := parCommit.Patch(c)
if err != nil {
return fmt.Errorf("failed to get patch: %w", err)
}
// Go over the filePatches (old/new pairs) and get create a
// map of charts with an slice of all the old/new fileDiffs
chartsWithMultipleFiles, err := getChartsWithMultipleChangedFiles(patch)
if err != nil {
return fmt.Errorf("failed to get changed files: %w", err)
}
// Go over the filePatches (old/new pairs) and get create a
// map of charts with an slice of all the old/new fileDiffs
chartsWithMultipleFiles, err := getChartsWithMultipleChangedFiles(patch)
if err != nil {
return fmt.Errorf("failed to get changed files: %w", err)
}
// For each chart, keep a single old/new pair, preferably the chart.yaml
// otherwise the first file in the list, doesn't matter
chartsWithSingleFile := getChartsWithSingleChangedFile(chartsWithMultipleFiles)
// For each chart, keep a single old/new pair, preferably the chart.yaml
// otherwise the first file in the list, doesn't matter
chartsWithSingleFile := getChartsWithSingleChangedFile(chartsWithMultipleFiles)
// Populate the changedData and stagingData
if err := processChartsWithSingleChangedFile(c, parCommit, chartsWithSingleFile); err != nil {
return fmt.Errorf("failed to process changed file: %w", err)
}
// Populate the changedData and stagingData
if err := processChartsWithSingleChangedFile(c, parCommit, chartsWithSingleFile); err != nil {
return fmt.Errorf("failed to process changed file: %w", err)
}
return nil
return nil
}
+124 -124
View File
@@ -1,155 +1,155 @@
package changelog
import (
"errors"
"fmt"
"strings"
"errors"
"fmt"
"strings"
"github.com/Masterminds/semver/v3"
"github.com/go-git/go-git/v5/plumbing/format/diff"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/rs/zerolog/log"
"github.com/Masterminds/semver/v3"
"github.com/go-git/go-git/v5/plumbing/format/diff"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/rs/zerolog/log"
)
var errSkipPatch = errors.New("skip patch")
func getChangedFilePair(p diff.FilePatch) (string, oldNewPaths, error) {
old, new := p.Files()
if new == nil { // No new file, nothing to do
log.Debug().Msgf("Skipping file patch. Reason: New file is empty")
return "", oldNewPaths{}, errSkipPatch
}
old, new := p.Files()
if new == nil { // No new file, nothing to do
log.Debug().Msgf("Skipping file patch. Reason: New file is empty")
return "", oldNewPaths{}, errSkipPatch
}
// Get chart name and check if its an active chart
// if the new.Path() is a path outside of the charts folder,
// it will not be an active chart anyway and so we skip the diff
chartName := getChartName(new.Path())
if chartName == invalidName || !activeCharts.isActiveChart(chartName) {
log.Debug().Msgf("Skipping file patch. Reason: [%s] is not an active chart", new.Path())
return "", oldNewPaths{}, errSkipPatch
}
if _, err := getChartPath(new.Path()); err != nil {
log.Debug().Msgf("Skipping file patch. Reason: [%s] is not a valid chart path", new.Path())
return "", oldNewPaths{}, errSkipPatch
}
if old != nil { // If an old file exists in the patch
if _, err := getChartPath(old.Path()); err != nil {
log.Debug().Msgf("Skipping file patch. Reason: [%s] is not a valid chart path", old.Path())
return "", oldNewPaths{}, errSkipPatch
}
}
// Get chart name and check if its an active chart
// if the new.Path() is a path outside of the charts folder,
// it will not be an active chart anyway and so we skip the diff
chartName := getChartName(new.Path())
if chartName == invalidName || !activeCharts.isActiveChart(chartName) {
log.Debug().Msgf("Skipping file patch. Reason: [%s] is not an active chart", new.Path())
return "", oldNewPaths{}, errSkipPatch
}
if _, err := getChartPath(new.Path()); err != nil {
log.Debug().Msgf("Skipping file patch. Reason: [%s] is not a valid chart path", new.Path())
return "", oldNewPaths{}, errSkipPatch
}
if old != nil { // If an old file exists in the patch
if _, err := getChartPath(old.Path()); err != nil {
log.Debug().Msgf("Skipping file patch. Reason: [%s] is not a valid chart path", old.Path())
return "", oldNewPaths{}, errSkipPatch
}
}
return chartName, oldNewPaths{new: new, old: old}, nil
return chartName, oldNewPaths{new: new, old: old}, nil
}
func getOldAndNewVersion(c *object.Commit, par *object.Commit, paths oldNewPaths) (string, string, error) {
newChartPath, err := getChartPath(paths.new.Path())
if err != nil {
return "", "", fmt.Errorf("failed to get chart path from file path [%s]: %w", paths.new.Path(), err)
}
newChartVer, err := getChartVersion(c, newChartPath)
if err != nil {
return "", "", fmt.Errorf("failed to get chart data from path [%s]: %w", newChartPath, err)
}
newChartPath, err := getChartPath(paths.new.Path())
if err != nil {
return "", "", fmt.Errorf("failed to get chart path from file path [%s]: %w", paths.new.Path(), err)
}
newChartVer, err := getChartVersion(c, newChartPath)
if err != nil {
return "", "", fmt.Errorf("failed to get chart data from path [%s]: %w", newChartPath, err)
}
oldChartVer := ""
if paths.old != nil { // If an old file exists in the patch
oldChartPath, err := getChartPath(paths.old.Path())
if err != nil {
return "", "", fmt.Errorf("failed to get chart path from file path [%s]: %w", paths.old.Path(), err)
}
// Note here we pass the parent commit, not the current commit
oldChartVer, err = getChartVersion(par, oldChartPath)
if err != nil {
return "", "", fmt.Errorf("failed to get chart data from path [%s]: %w", oldChartPath, err)
}
}
oldChartVer := ""
if paths.old != nil { // If an old file exists in the patch
oldChartPath, err := getChartPath(paths.old.Path())
if err != nil {
return "", "", fmt.Errorf("failed to get chart path from file path [%s]: %w", paths.old.Path(), err)
}
// Note here we pass the parent commit, not the current commit
oldChartVer, err = getChartVersion(par, oldChartPath)
if err != nil {
return "", "", fmt.Errorf("failed to get chart data from path [%s]: %w", oldChartPath, err)
}
}
return oldChartVer, newChartVer, nil
return oldChartVer, newChartVer, nil
}
func getChartsWithMultipleChangedFiles(p *object.Patch) (chartsWithChangedFiles, error) {
chartsWithMultipleFiles := make(chartsWithChangedFiles)
for _, p := range p.FilePatches() {
// Get chart name and the "new" file path
chartName, paths, err := getChangedFilePair(p)
if err != nil {
if errors.Is(err, errSkipPatch) {
continue
}
return chartsWithChangedFiles{}, fmt.Errorf("failed to get changed files: %w", err)
}
// if there is no new file, skip the filePatch
if paths.new.Path() == "" {
continue
}
chartsWithMultipleFiles := make(chartsWithChangedFiles)
for _, p := range p.FilePatches() {
// Get chart name and the "new" file path
chartName, paths, err := getChangedFilePair(p)
if err != nil {
if errors.Is(err, errSkipPatch) {
continue
}
return chartsWithChangedFiles{}, fmt.Errorf("failed to get changed files: %w", err)
}
// if there is no new file, skip the filePatch
if paths.new.Path() == "" {
continue
}
// Add the file to the charts changed files
chartsWithMultipleFiles[chartName] = append(chartsWithMultipleFiles[chartName], paths)
}
// Add the file to the charts changed files
chartsWithMultipleFiles[chartName] = append(chartsWithMultipleFiles[chartName], paths)
}
return chartsWithMultipleFiles, nil
return chartsWithMultipleFiles, nil
}
func getChartsWithSingleChangedFile(c chartsWithChangedFiles) chartsWithChangedFile {
chartsWithSingleFile := make(chartsWithChangedFile)
for chartName, filePaths := range c {
for _, paths := range filePaths {
_, ok := chartsWithSingleFile[chartName]
// If the chart hasn't been seen before,
// or the filePath is a Chart.yaml file
// we add the pair to the map
if !ok || strings.HasSuffix(paths.new.Path(), "Chart.yaml") {
chartsWithSingleFile[chartName] = paths
continue
}
}
}
return chartsWithSingleFile
chartsWithSingleFile := make(chartsWithChangedFile)
for chartName, filePaths := range c {
for _, paths := range filePaths {
_, ok := chartsWithSingleFile[chartName]
// If the chart hasn't been seen before,
// or the filePath is a Chart.yaml file
// we add the pair to the map
if !ok || strings.HasSuffix(paths.new.Path(), "Chart.yaml") {
chartsWithSingleFile[chartName] = paths
continue
}
}
}
return chartsWithSingleFile
}
func processChartsWithSingleChangedFile(c *object.Commit, par *object.Commit, chartsWithSingleFile chartsWithChangedFile) error {
// For each chart, get the old and new versions
for chartName, paths := range chartsWithSingleFile {
oldVer, newVer, err := getOldAndNewVersion(c, par, paths)
if err != nil {
return fmt.Errorf("failed to get old and new versions: %w", err)
}
// If the old version is empty, (chart addition)
// we add the new version to the changedData
if oldVer == "" {
changedData.mu.Lock()
changedData.AddOrUpdateChart(chartName, newVer, getChartTrain(paths.new.Path()), c)
changedData.mu.Unlock()
continue
}
// For each chart, get the old and new versions
for chartName, paths := range chartsWithSingleFile {
oldVer, newVer, err := getOldAndNewVersion(c, par, paths)
if err != nil {
return fmt.Errorf("failed to get old and new versions: %w", err)
}
// If the old version is empty, (chart addition)
// we add the new version to the changedData
if oldVer == "" {
changedData.mu.Lock()
changedData.AddOrUpdateChart(chartName, newVer, getChartTrain(paths.new.Path()), c)
changedData.mu.Unlock()
continue
}
oldSemVer, err := semver.NewVersion(oldVer)
if err != nil {
return fmt.Errorf("failed to parse old version ([%s]) for file [%s] in commit [%s]: %w", oldVer, paths.new.Path(), c.Hash.String(), err)
}
newSemVer, err := semver.NewVersion(newVer)
if err != nil {
return fmt.Errorf("failed to parse new version ([%s]) for file [%s] in commit [%s]: %w", newVer, paths.new.Path(), c.Hash.String(), err)
}
oldSemVer, err := semver.NewVersion(oldVer)
if err != nil {
return fmt.Errorf("failed to parse old version ([%s]) for file [%s] in commit [%s]: %w", oldVer, paths.new.Path(), c.Hash.String(), err)
}
newSemVer, err := semver.NewVersion(newVer)
if err != nil {
return fmt.Errorf("failed to parse new version ([%s]) for file [%s] in commit [%s]: %w", newVer, paths.new.Path(), c.Hash.String(), err)
}
// if new version is greater than the old version, we add the new version to the changedData
if newSemVer.GreaterThan(oldSemVer) {
changedData.mu.Lock()
changedData.AddOrUpdateChart(chartName, newVer, getChartTrain(paths.new.Path()), c)
changedData.mu.Unlock()
continue
}
// if new version is greater than the old version, we add the new version to the changedData
if newSemVer.GreaterThan(oldSemVer) {
changedData.mu.Lock()
changedData.AddOrUpdateChart(chartName, newVer, getChartTrain(paths.new.Path()), c)
changedData.mu.Unlock()
continue
}
// Otherwise, we add the new version to the stagingData
// It is probably less or equal to the old version,
// in either case the chart changes is unreleased.
// so it should go to the "next" version, we do that at the end
// although if its less, it will be hard to actually get which is the "next" version
// but we can't really do anything about it, so just put it on the immediate next version
stagingData.mu.Lock()
stagingData.AddOrUpdateChart(chartName, newVer, getChartTrain(paths.new.Path()), c)
stagingData.mu.Unlock()
}
return nil
// Otherwise, we add the new version to the stagingData
// It is probably less or equal to the old version,
// in either case the chart changes is unreleased.
// so it should go to the "next" version, we do that at the end
// although if its less, it will be hard to actually get which is the "next" version
// but we can't really do anything about it, so just put it on the immediate next version
stagingData.mu.Lock()
stagingData.AddOrUpdateChart(chartName, newVer, getChartTrain(paths.new.Path()), c)
stagingData.mu.Unlock()
}
return nil
}
+60 -60
View File
@@ -1,75 +1,75 @@
package changelog
import (
"bytes"
"html/template"
"os"
"path/filepath"
"sync"
"time"
"bytes"
"html/template"
"os"
"path/filepath"
"sync"
"time"
"github.com/rs/zerolog/log"
"github.com/truecharts/public/clustertool/pkg/helper"
"github.com/rs/zerolog/log"
"github.com/truecharts/public/clustertool/pkg/helper"
)
func (o *ChangelogOptions) Render() error {
start := time.Now()
log.Info().Msgf("Starting changelog render at %s", start)
start := time.Now()
log.Info().Msgf("Starting changelog render at %s", start)
changelogData := ChangedData{mu: &sync.RWMutex{}, Charts: make(map[string]*Chart)}
activeCharts := ActiveCharts{items: make(map[string]ActiveChart), mu: &sync.RWMutex{}}
if err := changelogData.LoadFromFile(o.JSONOutputPath); err != nil {
log.Fatal().Err(err).Msgf("failed to load %s", o.JSONOutputPath)
}
if err := helper.WalkCharts2([]string{o.RepoPath}, activeCharts.getActiveChartsWalker, helper.AsyncMode); err != nil {
log.Fatal().Err(err).Msg("failed to walk charts")
}
changelogData := ChangedData{mu: &sync.RWMutex{}, Charts: make(map[string]*Chart)}
activeCharts := ActiveCharts{items: make(map[string]ActiveChart), mu: &sync.RWMutex{}}
if err := changelogData.LoadFromFile(o.JSONOutputPath); err != nil {
log.Fatal().Err(err).Msgf("failed to load %s", o.JSONOutputPath)
}
if err := helper.WalkCharts2([]string{o.RepoPath}, activeCharts.getActiveChartsWalker, helper.AsyncMode); err != nil {
log.Fatal().Err(err).Msg("failed to walk charts")
}
for _, chart := range activeCharts.items {
if changelogData.Charts[chart.Name] == nil {
log.Error().Msgf("chart [%s] not found in %s", chart.Name, o.JSONOutputPath)
continue
for _, chart := range activeCharts.items {
if changelogData.Charts[chart.Name] == nil {
log.Error().Msgf("chart [%s] not found in %s", chart.Name, o.JSONOutputPath)
continue
}
if changelogData.Charts[chart.Name].Versions == nil {
log.Error().Msgf("chart [%s] has no versions in %s", chart.Name, o.JSONOutputPath)
continue
}
// load template
tmpl, err := template.ParseFiles(o.TemplatePath)
if err != nil {
log.Fatal().Err(err).Msgf("failed to parse %s", o.TemplatePath)
}
}
if changelogData.Charts[chart.Name].Versions == nil {
log.Error().Msgf("chart [%s] has no versions in %s", chart.Name, o.JSONOutputPath)
continue
}
// load template
tmpl, err := template.ParseFiles(o.TemplatePath)
if err != nil {
log.Fatal().Err(err).Msgf("failed to parse %s", o.TemplatePath)
}
if _, err := changelogData.Charts[chart.Name].SortVersions(true); err != nil {
log.Fatal().Err(err).Msgf("failed to sort versions for %s", chart.Name)
}
for _, version := range changelogData.Charts[chart.Name].Versions {
version.SortedCommits, err = version.SortCommits(true)
if err != nil {
log.Fatal().Err(err).Msgf("failed to sort commits for version [%s] in chart [%s]", version.Version, chart.Name)
}
}
if _, err := changelogData.Charts[chart.Name].SortVersions(true); err != nil {
log.Fatal().Err(err).Msgf("failed to sort versions for %s", chart.Name)
}
for _, version := range changelogData.Charts[chart.Name].Versions {
version.SortedCommits, err = version.SortCommits(true)
if err != nil {
log.Fatal().Err(err).Msgf("failed to sort commits for version [%s] in chart [%s]", version.Version, chart.Name)
}
}
changelogData.Charts[chart.Name].Name = chart.Name
changelogData.Charts[chart.Name].Train = chart.Train
// render template
var buf bytes.Buffer
err = tmpl.Execute(&buf, changelogData.Charts[chart.Name])
if err != nil {
log.Fatal().Err(err).Msgf("failed to render %s", o.TemplatePath)
}
changelogData.Charts[chart.Name].Name = chart.Name
changelogData.Charts[chart.Name].Train = chart.Train
// render template
var buf bytes.Buffer
err = tmpl.Execute(&buf, changelogData.Charts[chart.Name])
if err != nil {
log.Fatal().Err(err).Msgf("failed to render %s", o.TemplatePath)
}
output := filepath.Join(o.ChartsDir, chart.Train, chart.Name)
if err := os.MkdirAll(output, os.ModePerm); err != nil {
log.Fatal().Err(err).Msgf("failed to create %s directory", output)
}
// write rendered template to file
if err := os.WriteFile(filepath.Join(output, o.ChangelogFileName), buf.Bytes(), 0644); err != nil {
log.Fatal().Err(err).Msgf("failed to write %s", o.ChangelogFileName)
}
}
output := filepath.Join(o.ChartsDir, chart.Train, chart.Name)
if err := os.MkdirAll(output, os.ModePerm); err != nil {
log.Fatal().Err(err).Msgf("failed to create %s directory", output)
}
// write rendered template to file
if err := os.WriteFile(filepath.Join(output, o.ChangelogFileName), buf.Bytes(), 0644); err != nil {
log.Fatal().Err(err).Msgf("failed to write %s", o.ChangelogFileName)
}
}
log.Info().Msgf("Finished in %s", time.Since(start))
return nil
log.Info().Msgf("Finished in %s", time.Since(start))
return nil
}
+108 -108
View File
@@ -1,128 +1,128 @@
package changelog
import (
"errors"
"fmt"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"errors"
"fmt"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/rs/zerolog/log"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/rs/zerolog/log"
)
type status struct {
processedCount int
totalCount int
skippedCount int
avgTime time.Duration
totalProcessingTime time.Duration
mu *sync.RWMutex
processedCount int
totalCount int
skippedCount int
avgTime time.Duration
totalProcessingTime time.Duration
mu *sync.RWMutex
}
func (s *status) incSkippedCount() {
s.mu.Lock()
defer s.mu.Unlock()
s.skippedCount++
s.processedCount--
s.mu.Lock()
defer s.mu.Unlock()
s.skippedCount++
s.processedCount--
}
func (o *ChangelogOptions) printStatus(start time.Time, eta bool) {
currentStatus.mu.RLock()
defer currentStatus.mu.RUnlock()
if eta {
log.Info().Msgf("Processed [%d + (%d skipped) / %d] commits in %s, ETA: %s (avg commit processing time: %s)", currentStatus.processedCount, currentStatus.skippedCount, currentStatus.totalCount, time.Since(start), time.Duration(currentStatus.totalCount-currentStatus.processedCount-currentStatus.skippedCount)*currentStatus.avgTime, currentStatus.avgTime)
} else {
log.Info().Msgf("Processed [%d + (%d skipped) / %d] commits in %s", currentStatus.processedCount, currentStatus.skippedCount, currentStatus.totalCount, time.Since(start))
}
currentStatus.mu.RLock()
defer currentStatus.mu.RUnlock()
if eta {
log.Info().Msgf("Processed [%d + (%d skipped) / %d] commits in %s, ETA: %s (avg commit processing time: %s)", currentStatus.processedCount, currentStatus.skippedCount, currentStatus.totalCount, time.Since(start), time.Duration(currentStatus.totalCount-currentStatus.processedCount-currentStatus.skippedCount)*currentStatus.avgTime, currentStatus.avgTime)
} else {
log.Info().Msgf("Processed [%d + (%d skipped) / %d] commits in %s", currentStatus.processedCount, currentStatus.skippedCount, currentStatus.totalCount, time.Since(start))
}
}
func (o *ChangelogOptions) statusPrinter(stop <-chan struct{}) {
log.Info().Msgf("Printing status every [%d] seconds", o.StatusUpdateInterval)
start := time.Now()
ticker := time.NewTicker(time.Second * time.Duration(o.StatusUpdateInterval))
defer ticker.Stop()
for {
select {
case <-ticker.C:
o.printStatus(start, true)
case <-stop:
return
}
}
log.Info().Msgf("Printing status every [%d] seconds", o.StatusUpdateInterval)
start := time.Now()
ticker := time.NewTicker(time.Second * time.Duration(o.StatusUpdateInterval))
defer ticker.Stop()
for {
select {
case <-ticker.C:
o.printStatus(start, true)
case <-stop:
return
}
}
}
func (o *ChangelogOptions) reverseCommits(cIter object.CommitIter, lastCommit string) ([]*object.Commit, error) {
start := time.Now()
var commits []*object.Commit
var errDoneReversing = errors.New("done reversing commits")
log.Info().Msgf("Reversing commits order", )
defer cIter.Close()
if err := cIter.ForEach(func(c *object.Commit) error {
// We go from newer to oldest, if we hit the last commit, we stop
if c.Hash.String() == lastCommit {
return errDoneReversing
}
start := time.Now()
var commits []*object.Commit
var errDoneReversing = errors.New("done reversing commits")
log.Info().Msgf("Reversing commits order", )
defer cIter.Close()
if err := cIter.ForEach(func(c *object.Commit) error {
// We go from newer to oldest, if we hit the last commit, we stop
if c.Hash.String() == lastCommit {
return errDoneReversing
}
currentStatus.totalCount++
// Reverse the order of the commits to get the oldest first
commits = append([]*object.Commit{c}, commits...)
return nil
}); err != nil {
if !errors.Is(err, errDoneReversing) {
return nil, err
}
}
currentStatus.totalCount++
// Reverse the order of the commits to get the oldest first
commits = append([]*object.Commit{c}, commits...)
return nil
}); err != nil {
if !errors.Is(err, errDoneReversing) {
return nil, err
}
}
log.Info().Msgf("Finished reversing commits in %s", time.Since(start))
return commits, nil
log.Info().Msgf("Finished reversing commits in %s", time.Since(start))
return commits, nil
}
// Just some random text to avoid any chart name conflicts
var invalidName = "5fdad45c8f5b954e5643c314"
func getChartName(path string) string {
// path = charts/<train>/<chart>/...
parts := strings.Split(path, "/")
if len(parts) < 3 {
log.Debug().Msgf("failed to get chart name from path [%s]", path)
return invalidName
}
return parts[2]
// path = charts/<train>/<chart>/...
parts := strings.Split(path, "/")
if len(parts) < 3 {
log.Debug().Msgf("failed to get chart name from path [%s]", path)
return invalidName
}
return parts[2]
}
var chartFilePathRegex = regexp.MustCompile(`^charts/([\w-_]+)/([\w-_]+)/Chart.yaml$`)
func getChartPath(path string) (string, error) {
original := path
for {
if path == "." {
return "", fmt.Errorf("path too short [%s], or could not construct chart path", original)
}
if chartFilePathRegex.MatchString(filepath.Join(path, "Chart.yaml")) {
return filepath.Join(path, "Chart.yaml"), nil
}
// Remove the last part of the path and try again
path = filepath.Dir(path)
}
original := path
for {
if path == "." {
return "", fmt.Errorf("path too short [%s], or could not construct chart path", original)
}
if chartFilePathRegex.MatchString(filepath.Join(path, "Chart.yaml")) {
return filepath.Join(path, "Chart.yaml"), nil
}
// Remove the last part of the path and try again
path = filepath.Dir(path)
}
}
func getChartVersion(c *object.Commit, path string) (string, error) {
tree, err := c.Tree()
if err != nil {
return "", fmt.Errorf("failed to get tree: %w", err)
}
file, err := tree.File(path)
if err != nil {
return "", fmt.Errorf("failed to get file: %w", err)
}
strData, err := file.Contents()
if err != nil {
return "", fmt.Errorf("failed to get file contents: %w", err)
}
return getVersion(strData)
tree, err := c.Tree()
if err != nil {
return "", fmt.Errorf("failed to get tree: %w", err)
}
file, err := tree.File(path)
if err != nil {
return "", fmt.Errorf("failed to get file: %w", err)
}
strData, err := file.Contents()
if err != nil {
return "", fmt.Errorf("failed to get file contents: %w", err)
}
return getVersion(strData)
}
var charsToRemove = []string{"-"}
@@ -130,25 +130,25 @@ var charsToRemove = []string{"-"}
// We use this instead of NewHelmChart.Load(), because
// this will work even if the Chart.yaml is malformed
func getVersion(strData string) (string, error) {
lines := strings.Split(strData, "\n")
for _, line := range lines {
if strings.HasPrefix(line, "version:") {
ver := strings.TrimSpace(strings.Split(line, ":")[1])
// In some cases there was a type in the version (eg "1.0.-2")
for _, c := range charsToRemove {
ver = strings.ReplaceAll(ver, c, "")
}
return ver, nil
}
}
return "", fmt.Errorf("could not find version in file")
lines := strings.Split(strData, "\n")
for _, line := range lines {
if strings.HasPrefix(line, "version:") {
ver := strings.TrimSpace(strings.Split(line, ":")[1])
// In some cases there was a type in the version (eg "1.0.-2")
for _, c := range charsToRemove {
ver = strings.ReplaceAll(ver, c, "")
}
return ver, nil
}
}
return "", fmt.Errorf("could not find version in file")
}
func getChartTrain(path string) string {
parts := strings.Split(path, "/")
if len(parts) < 2 {
log.Error().Msgf("Could not get chart train from path [%s]", path)
return ""
}
return parts[1]
parts := strings.Split(path, "/")
if len(parts) < 2 {
log.Error().Msgf("Could not get chart train from path [%s]", path)
return ""
}
return parts[1]
}
+131 -131
View File
@@ -1,227 +1,227 @@
package chartFile
import (
"bytes"
"fmt"
"os"
"bytes"
"fmt"
"os"
"github.com/go-playground/validator/v10"
"github.com/truecharts/public/clustertool/pkg/helper"
"github.com/go-playground/validator/v10"
"github.com/truecharts/public/clustertool/pkg/helper"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
"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"
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"`
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"`
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
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
K *koanf.Koanf
Metadata ChartMetadata `yaml:"metadata" validate:"required,dive"`
// Add other fields as needed
}
func NewHelmChart() *HelmChart {
return &HelmChart{
K: koanf.New("."),
}
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)
}
// 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)
}
// 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()
// Set default values for fields if they are not set or empty
h.setDefaultValues()
// Initialize validator
validate = validator.New(validator.WithRequiredStructEnabled())
// Initialize validator
validate = validator.New(validator.WithRequiredStructEnabled())
if err := validate.Struct(h.Metadata); err != nil {
return fmt.Errorf("chart.yaml validation error: %v", err)
}
if err := validate.Struct(h.Metadata); err != nil {
return fmt.Errorf("chart.yaml validation error: %v", err)
}
return nil
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.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,
})
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)
}
// 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)
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
// 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())
// Initialize validator
validate = validator.New(validator.WithRequiredStructEnabled())
if err := validate.Struct(h.Metadata); err != nil {
return fmt.Errorf("chart.yaml validation error: %v", err)
}
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)
}
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)
}
// 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
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
}
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
}
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
}
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
}
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
}
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
}
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
}
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
h.Metadata.APIVersion = apiVersion
}
// setKubeVersion sets the kubeVersion field to kubeVersion
func (h *HelmChart) setKubeVersion(kubeVersion string) {
h.Metadata.KubeVersion = kubeVersion
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
h.Metadata.Maintainers = make([]Maintainer, 1)
h.Metadata.Maintainers[0] = maintainers
}
File diff suppressed because it is too large Load Diff
+157 -157
View File
@@ -1,208 +1,208 @@
package chartFile
import (
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"github.com/rs/zerolog/log"
"github.com/truecharts/public/clustertool/pkg/charts/helmignore"
"github.com/truecharts/public/clustertool/pkg/charts/image"
"github.com/truecharts/public/clustertool/pkg/charts/readme"
"github.com/truecharts/public/clustertool/pkg/charts/version"
"github.com/rs/zerolog/log"
"github.com/truecharts/public/clustertool/pkg/charts/helmignore"
"github.com/truecharts/public/clustertool/pkg/charts/image"
"github.com/truecharts/public/clustertool/pkg/charts/readme"
"github.com/truecharts/public/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
}
fileInfo, err := os.Stat(chartPathOrFolder)
if err != nil {
return err
}
chartPath := chartPathOrFolder
if fileInfo.IsDir() {
chartPath = filepath.Join(chartPathOrFolder, "Chart.yaml")
}
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
}
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)
}
if chart.Metadata.Annotations == nil {
chart.Metadata.Annotations = make(map[string]string)
}
train := GetTrain(chartPath, chart)
setMetadata(chart, train)
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 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)
}
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
}
// 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
}
// 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)
}
// 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)
log.Info().Msgf("Chart file updated and saved to [%s]", chartPath)
templateDir := chartPath
for i := 0; i < 4; i++ {
templateDir = filepath.Dir(templateDir)
}
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 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
// 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
}
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
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),
)
parts := strings.Split(
// Remove the filename from the path
filepath.Dir(chartFilename),
string(os.PathSeparator),
)
if len(parts) >= 2 {
return parts[len(parts)-2]
}
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 ""
// 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"
}
}
// 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
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)
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
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)
}
}
}
// 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 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...)
// 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
}
// 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)
}
// 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)
// Sort the sources, so subsequent commits will only include actual changes
slices.Sort(finalSources)
// Update the chart's sources
chart.Metadata.Sources = finalSources
// Update the chart's sources
chart.Metadata.Sources = finalSources
return nil
return nil
}
+203 -203
View File
@@ -1,230 +1,230 @@
package chartFile
import (
"reflect"
"testing"
"reflect"
"testing"
"github.com/truecharts/public/clustertool/pkg/charts/image"
"github.com/truecharts/public/clustertool/pkg/charts/image"
)
func TestSetAppVersionFromImage(t *testing.T) {
type TestData struct {
chart *HelmChart
image *image.Images
key string
result string
}
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",
},
}
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)
}
}
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
}
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",
},
}
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)
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)
}
})
}
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
}
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",
},
},
},
}
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)
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)
}
})
}
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
}
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",
},
},
}
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)
}
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)
}
})
}
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)
}
})
}
}
+139 -139
View File
@@ -1,197 +1,197 @@
package deps
import (
"fmt"
"io"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"fmt"
"io"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog/log"
"github.com/truecharts/public/clustertool/pkg/charts/chartFile"
"github.com/truecharts/public/clustertool/pkg/fluxhandler"
"github.com/truecharts/public/clustertool/pkg/helper"
"github.com/truecharts/public/clustertool/pkg/charts/chartFile"
"github.com/truecharts/public/clustertool/pkg/fluxhandler"
"github.com/truecharts/public/clustertool/pkg/helper"
)
func LoadGPGKey() error {
log.Info().Msg("🔑 Fetching and Loading TrueCharts PGP Public Key 🔑")
if err := os.MkdirAll(helper.GpgDir, os.ModePerm); err != nil {
log.Fatal().Err(err).Msg("❌ Failed to create GPG directory")
}
log.Info().Msg("🔑 Fetching and Loading TrueCharts PGP Public Key 🔑")
if err := os.MkdirAll(helper.GpgDir, os.ModePerm); err != nil {
log.Fatal().Err(err).Msg("❌ Failed to create GPG directory")
}
keybaseURL := "https://truecharts.org/pub_key.gpg"
pubringPath := path.Join(helper.GpgDir, "pubring.gpg")
if err := downloadFile(keybaseURL, pubringPath); err != nil {
log.Fatal().Err(err).Msg("❌ Failed to download keybase public key")
}
keybaseURL := "https://truecharts.org/pub_key.gpg"
pubringPath := path.Join(helper.GpgDir, "pubring.gpg")
if err := downloadFile(keybaseURL, pubringPath); err != nil {
log.Fatal().Err(err).Msg("❌ Failed to download keybase public key")
}
certmanURL := "https://cert-manager.io/public-keys/cert-manager-keyring-2021-09-20-1020CF3C033D4F35BAE1C19E1226061C665DF13E.gpg"
certmanPath := path.Join(helper.GpgDir, "certman.gpg")
if err := downloadFile(certmanURL, certmanPath); err != nil {
log.Fatal().Err(err).Msg("❌ Failed to download certman public key")
}
certmanURL := "https://cert-manager.io/public-keys/cert-manager-keyring-2021-09-20-1020CF3C033D4F35BAE1C19E1226061C665DF13E.gpg"
certmanPath := path.Join(helper.GpgDir, "certman.gpg")
if err := downloadFile(certmanURL, certmanPath); err != nil {
log.Fatal().Err(err).Msg("❌ Failed to download certman public key")
}
log.Info().Msg("✅ Public Key loaded successfully")
return nil
log.Info().Msg("✅ Public Key loaded successfully")
return nil
}
func downloadFile(url, destination string) error {
response, err := http.Get(url)
if err != nil {
log.Error().Err(err).Msgf("❌ Failed to download [%s]", url)
return err
}
defer response.Body.Close()
response, err := http.Get(url)
if err != nil {
log.Error().Err(err).Msgf("❌ Failed to download [%s]", url)
return err
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
log.Error().Err(err).Msg("❌ Failed to read response body")
return err
}
body, err := io.ReadAll(response.Body)
if err != nil {
log.Error().Err(err).Msg("❌ Failed to read response body")
return err
}
err = os.WriteFile(destination, body, os.ModePerm)
if err != nil {
log.Error().Err(err).Msgf("❌ Failed to write file at [%s]", destination)
return err
}
err = os.WriteFile(destination, body, os.ModePerm)
if err != nil {
log.Error().Err(err).Msgf("❌ Failed to write file at [%s]", destination)
return err
}
return nil
return nil
}
// fetchIndexFile downloads an index file from a repo if not already cached
func fetchIndexFile(repo string, repoDir string, repoURL string) error {
destPath := path.Join(helper.IndexCache, repoDir, "index.yaml")
if strings.HasPrefix(repoURL, "oci") {
log.Info().Msgf("⏩ URL [%s] is OCI, skipping index download", repoURL)
return nil
}
destPath := path.Join(helper.IndexCache, repoDir, "index.yaml")
if strings.HasPrefix(repoURL, "oci") {
log.Info().Msgf("⏩ URL [%s] is OCI, skipping index download", repoURL)
return nil
}
if _, err := os.Stat(destPath); err == nil {
log.Info().Msgf("✅ Index file for [%s] already cached", repo)
return nil
}
if _, err := os.Stat(destPath); err == nil {
log.Info().Msgf("✅ Index file for [%s] already cached", repo)
return nil
}
log.Info().Msgf("🙅 Index file for [%s] not cached", repo)
log.Info().Msgf("🙅 Index file for [%s] not cached", repo)
// Create index directory
err := os.MkdirAll(path.Join(helper.IndexCache, repoDir), os.ModePerm)
if err != nil {
log.Fatal().Err(err).Msg("❌ Failed to create index directory")
}
// Create index directory
err := os.MkdirAll(path.Join(helper.IndexCache, repoDir), os.ModePerm)
if err != nil {
log.Fatal().Err(err).Msg("❌ Failed to create index directory")
}
// Download index file
log.Info().Msgf("⏬ Downloading index [%s]...", repoURL)
err = downloadFile(repoURL, destPath)
if err != nil {
log.Fatal().Err(err).Msgf("❌ Failed to download index for [%s] from [%s]", repo, repoURL)
}
// Download index file
log.Info().Msgf("⏬ Downloading index [%s]...", repoURL)
err = downloadFile(repoURL, destPath)
if err != nil {
log.Fatal().Err(err).Msgf("❌ Failed to download index for [%s] from [%s]", repo, repoURL)
}
log.Info().Msg("✅ Index File downloaded")
log.Info().Msg("✅ Index File downloaded")
return nil
return nil
}
// fetchDependency downloads a dependency from a repo if not already cached
func fetchDependency(repo string, repoDir string, name string, version string, repoURL string) error {
destPath := path.Join(helper.HelmCache, repoDir, fmt.Sprintf("%s-%s.tgz", name, version))
if _, err := os.Stat(destPath); err == nil {
log.Info().Msgf("✅ Dependency [%s-%s] already cached", name, version)
return nil
}
destPath := path.Join(helper.HelmCache, repoDir, fmt.Sprintf("%s-%s.tgz", name, version))
if _, err := os.Stat(destPath); err == nil {
log.Info().Msgf("✅ Dependency [%s-%s] already cached", name, version)
return nil
}
log.Info().Msgf("🙅 Dependency [%s-%s] not cached", name, version)
log.Info().Msgf("🙅 Dependency [%s-%s] not cached", name, version)
repoCacheDir := path.Join(helper.HelmCache, repoDir)
// Create cache directory
if err := os.MkdirAll(repoCacheDir, os.ModePerm); err != nil {
return fmt.Errorf("❌ Failed to create cache directory: %s", err)
}
repoCacheDir := path.Join(helper.HelmCache, repoDir)
// Create cache directory
if err := os.MkdirAll(repoCacheDir, os.ModePerm); err != nil {
return fmt.Errorf("❌ Failed to create cache directory: %s", err)
}
// Download dependency
log.Info().Msgf("⏬ Downloading dependency [%s-%s] from [%s]", name, version, repo)
if err := fluxhandler.HelmPull(repo, name, version, repoCacheDir, false); err != nil {
return fmt.Errorf("❌ Failed to download or verify dependency: %s", err)
}
// Download dependency
log.Info().Msgf("⏬ Downloading dependency [%s-%s] from [%s]", name, version, repo)
if err := fluxhandler.HelmPull(repo, name, version, repoCacheDir, false); err != nil {
return fmt.Errorf("❌ Failed to download or verify dependency: %s", err)
}
log.Info().Msg("✅ Dependency downloaded")
log.Info().Msg("✅ Dependency downloaded")
return nil
return nil
}
// copyDependency copies a dependency from the cache to the chart folder
func copyDependency(chartFolder string, repo string, repoDir string, name string, version string) error {
log.Info().Msg("📝 Copying dependency")
log.Info().Msg("📝 Copying dependency")
targetChartsFolder := path.Join(chartFolder, "charts")
if err := os.MkdirAll(targetChartsFolder, os.ModePerm); err != nil {
return fmt.Errorf("❌ Failed to create charts directory: %s", err)
}
targetChartsFolder := path.Join(chartFolder, "charts")
if err := os.MkdirAll(targetChartsFolder, os.ModePerm); err != nil {
return fmt.Errorf("❌ Failed to create charts directory: %s", err)
}
srcPath := path.Join(helper.HelmCache, repoDir, fmt.Sprintf("%s-%s.tgz", name, version))
destPath := path.Join(targetChartsFolder, fmt.Sprintf("%s-%s.tgz", name, version))
if err := helper.CopyFile(srcPath, destPath, false); err != nil {
return fmt.Errorf("❌ Failed to copy dependency: %s", err)
}
srcPath := path.Join(helper.HelmCache, repoDir, fmt.Sprintf("%s-%s.tgz", name, version))
destPath := path.Join(targetChartsFolder, fmt.Sprintf("%s-%s.tgz", name, version))
if err := helper.CopyFile(srcPath, destPath, false); err != nil {
return fmt.Errorf("❌ Failed to copy dependency: %s", err)
}
log.Info().Msg("✅ Dependency copied!")
return nil
log.Info().Msg("✅ Dependency copied!")
return nil
}
func DownloadDeps(chartPath string, placeholder string) error {
chartFolder := filepath.Dir(chartPath)
chartFolder := filepath.Dir(chartPath)
helmChart := chartFile.NewHelmChart()
err := helmChart.LoadFromFile(chartPath)
if err != nil {
log.Fatal().Err(err).Msgf("❌ Failed to load Helm chart from file in [%s]", chartFolder)
}
helmChart := chartFile.NewHelmChart()
err := helmChart.LoadFromFile(chartPath)
if err != nil {
log.Fatal().Err(err).Msgf("❌ Failed to load Helm chart from file in [%s]", chartFolder)
}
fmt.Print("\n\n")
log.Info().Msgf("🏃 Processing Chart [%s] with [%d] dependencies", chartFolder, len(helmChart.Metadata.Dependencies))
fmt.Print("\n\n")
log.Info().Msgf("🏃 Processing Chart [%s] with [%d] dependencies", chartFolder, len(helmChart.Metadata.Dependencies))
// Make sure the directory "charts" exists in the chart folder
targetChartsFolder := path.Join(chartFolder, "charts")
if err := os.MkdirAll(targetChartsFolder, os.ModePerm); err != nil {
return fmt.Errorf("❌ Failed to create charts directory: %s", err)
}
// Make sure the directory "charts" exists in the chart folder
targetChartsFolder := path.Join(chartFolder, "charts")
if err := os.MkdirAll(targetChartsFolder, os.ModePerm); err != nil {
return fmt.Errorf("❌ Failed to create charts directory: %s", err)
}
// Process dependencies as needed
for _, dep := range helmChart.Metadata.Dependencies {
name := dep.Name
version := dep.Version
repo := dep.Repository
repoURL := fmt.Sprintf("%s/index.yaml", strings.TrimRight(repo, "/"))
// Process dependencies as needed
for _, dep := range helmChart.Metadata.Dependencies {
name := dep.Name
version := dep.Version
repo := dep.Repository
repoURL := fmt.Sprintf("%s/index.yaml", strings.TrimRight(repo, "/"))
fmt.Print("\n")
log.Info().Msgf("📦 Dependency [%s]", name)
log.Info().Msgf("🆚 Version [%s]", version)
log.Info().Msgf("📥 Repo [%s]", repo)
log.Info().Msgf("🔗 URL [%s]", repoURL)
fmt.Print("\n")
log.Info().Msgf("📦 Dependency [%s]", name)
log.Info().Msgf("🆚 Version [%s]", version)
log.Info().Msgf("📥 Repo [%s]", repo)
log.Info().Msgf("🔗 URL [%s]", repoURL)
repoDir := repo
// Remove protocol(s) from repoDir
for _, prefix := range []string{"http://", "https://", "oci://"} {
repoDir = strings.TrimPrefix(repoDir, prefix)
}
repoDir := repo
// Remove protocol(s) from repoDir
for _, prefix := range []string{"http://", "https://", "oci://"} {
repoDir = strings.TrimPrefix(repoDir, prefix)
}
if err := fetchIndexFile(repo, repoDir, repoURL); err != nil {
return fmt.Errorf("❌ Failed to fetch index file: %s", err)
}
if err := fetchIndexFile(repo, repoDir, repoURL); err != nil {
return fmt.Errorf("❌ Failed to fetch index file: %s", err)
}
if err := fetchDependency(repo, repoDir, name, version, repoURL); err != nil {
log.Fatal().Err(err).Msg("❌ Failed to fetch dependency")
}
if err := fetchDependency(repo, repoDir, name, version, repoURL); err != nil {
log.Fatal().Err(err).Msg("❌ Failed to fetch dependency")
}
if err := copyDependency(chartFolder, repo, repoDir, name, version); err != nil {
log.Fatal().Err(err).Msg("❌ Failed to copy dependency")
}
if err := copyDependency(chartFolder, repo, repoDir, name, version); err != nil {
log.Fatal().Err(err).Msg("❌ Failed to copy dependency")
}
log.Info().Msg("✅ Dependency processed!")
}
log.Info().Msg("✅ Dependency processed!")
}
log.Info().Msg("✅ Processing complete!")
return nil
log.Info().Msg("✅ Processing complete!")
return nil
}
+19 -19
View File
@@ -1,30 +1,30 @@
package helmignore
import (
"fmt"
"os"
"path/filepath"
"fmt"
"os"
"path/filepath"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog/log"
)
func GenerateHelmIgnore(templatePath string, chartPath string) error {
// Define file paths
template := filepath.Join(templatePath, "templates/helmignore.tpl")
target := filepath.Join(filepath.Dir(chartPath), ".helmignore")
// Define file paths
template := filepath.Join(templatePath, "templates/helmignore.tpl")
target := filepath.Join(filepath.Dir(chartPath), ".helmignore")
// Read template file
templateContent, err := os.ReadFile(template)
if err != nil {
return fmt.Errorf("failed to read template file: %v", err)
}
// Read template file
templateContent, err := os.ReadFile(template)
if err != nil {
return fmt.Errorf("failed to read template file: %v", err)
}
// Write the modified content to the .helmignore file in the chart directory
err = os.WriteFile(target, []byte(templateContent), 0644)
if err != nil {
return fmt.Errorf("failed to write .helmignore file: %v", err)
}
// Write the modified content to the .helmignore file in the chart directory
err = os.WriteFile(target, []byte(templateContent), 0644)
if err != nil {
return fmt.Errorf("failed to write .helmignore file: %v", err)
}
log.Info().Msgf("Generated .helmignore for [%s]", chartPath)
return nil
log.Info().Msgf("Generated .helmignore for [%s]", chartPath)
return nil
}
+112 -112
View File
@@ -1,187 +1,187 @@
package image
import (
"fmt"
"regexp"
"strings"
"fmt"
"regexp"
"strings"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation"
)
var (
// Valid SemVer format (Major.Minor.Patch)
semVerPattern = regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)$`)
// Matches tags like "RELEASE.2023-11-20T22-40-07Z"
releasePattern = regexp.MustCompile(`^RELEASE\.[0-9]{4}-[0-9]{2}-[0-9]{2}T`)
// Matches tags like "x64-1.2.3" and "arm64-1.2.3" followed by a numeric version
archPattern = regexp.MustCompile(`^[a-zA-Z0-9]+-[0-9]+\.[0-9]+`)
// Matches tags like "latest-2023-12-18"
prefixYearMonthDayPattern = regexp.MustCompile(`^[a-zA-Z0-9]+-[0-9]{4}-[0-9]{2}-[0-9]{2}$`)
// Matches dates like "2023-11-15" and "2022-04"
yearMonthDayPattern = regexp.MustCompile(`^[0-9]{4}-[0-9]{2}(-[0-9]{2})?$`)
// Matches tags like "1.2.3.4" "1.2" and "1"
incompleteSemVerPattern = regexp.MustCompile(`^[0-9]+(\.[0-9]+)*$`)
// Matches tags like "something-abcdefg" (only chars before dash and exactly 7 characters after the dash)
shortCommitHashSuffixPattern = regexp.MustCompile(`^[a-zA-Z]+-[a-zA-Z0-9]{7}$`)
// Matches tags like "v1.2.3", "V1.2.3, #1.2.3, $1.2.3, etc"
leadingSymbolPattern = regexp.MustCompile(`^version|Version|[vV]|^[^a-zA-Z0-9]+`)
// Valid SemVer format (Major.Minor.Patch)
semVerPattern = regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)$`)
// Matches tags like "RELEASE.2023-11-20T22-40-07Z"
releasePattern = regexp.MustCompile(`^RELEASE\.[0-9]{4}-[0-9]{2}-[0-9]{2}T`)
// Matches tags like "x64-1.2.3" and "arm64-1.2.3" followed by a numeric version
archPattern = regexp.MustCompile(`^[a-zA-Z0-9]+-[0-9]+\.[0-9]+`)
// Matches tags like "latest-2023-12-18"
prefixYearMonthDayPattern = regexp.MustCompile(`^[a-zA-Z0-9]+-[0-9]{4}-[0-9]{2}-[0-9]{2}$`)
// Matches dates like "2023-11-15" and "2022-04"
yearMonthDayPattern = regexp.MustCompile(`^[0-9]{4}-[0-9]{2}(-[0-9]{2})?$`)
// Matches tags like "1.2.3.4" "1.2" and "1"
incompleteSemVerPattern = regexp.MustCompile(`^[0-9]+(\.[0-9]+)*$`)
// Matches tags like "something-abcdefg" (only chars before dash and exactly 7 characters after the dash)
shortCommitHashSuffixPattern = regexp.MustCompile(`^[a-zA-Z]+-[a-zA-Z0-9]{7}$`)
// Matches tags like "v1.2.3", "V1.2.3, #1.2.3, $1.2.3, etc"
leadingSymbolPattern = regexp.MustCompile(`^version|Version|[vV]|^[^a-zA-Z0-9]+`)
)
func CleanTag(tag string) (string, error) {
tag = strings.TrimSpace(tag)
tag = strings.TrimSpace(tag)
if tag == "" {
return "", fmt.Errorf("tag is empty")
}
if tag == "" {
return "", fmt.Errorf("tag is empty")
}
// Do basic cleaning
tag = cleanSha(tag)
tag = cleanLeadingSymbol(tag)
// Do basic cleaning
tag = cleanSha(tag)
tag = cleanLeadingSymbol(tag)
// Return early if the tag is already in SemVer format
if semVerPattern.MatchString(tag) {
return tag, nil
}
// Return early if the tag is already in SemVer format
if semVerPattern.MatchString(tag) {
return tag, nil
}
switch {
case releasePattern.MatchString(tag):
tag = cleanRelease(tag)
case archPattern.MatchString(tag):
tag = cleanArch(tag)
case prefixYearMonthDayPattern.MatchString(tag):
tag = cleanPrefixYearMonthDay(tag)
case yearMonthDayPattern.MatchString(tag):
tag = cleanYearMonthDay(tag)
case incompleteSemVerPattern.MatchString(tag):
tag = cleanIncompleteSemVer(tag)
case shortCommitHashSuffixPattern.MatchString(tag):
tag = keepShortCommitHashSuffix(tag)
case leadingSymbolPattern.MatchString(tag):
tag = cleanLeadingSymbol(tag)
}
switch {
case releasePattern.MatchString(tag):
tag = cleanRelease(tag)
case archPattern.MatchString(tag):
tag = cleanArch(tag)
case prefixYearMonthDayPattern.MatchString(tag):
tag = cleanPrefixYearMonthDay(tag)
case yearMonthDayPattern.MatchString(tag):
tag = cleanYearMonthDay(tag)
case incompleteSemVerPattern.MatchString(tag):
tag = cleanIncompleteSemVer(tag)
case shortCommitHashSuffixPattern.MatchString(tag):
tag = keepShortCommitHashSuffix(tag)
case leadingSymbolPattern.MatchString(tag):
tag = cleanLeadingSymbol(tag)
}
// If string contains `-` the second part is usually
// either a commit hash or things like "debian" or "alpine"
// Make sure the first part is some kind of versioning and strip the rest
if strings.Contains(tag, "-") {
split := strings.Split(tag, "-")
switch {
case semVerPattern.MatchString(split[0]):
tag = split[0]
case incompleteSemVerPattern.MatchString(split[0]):
tag = split[0]
}
}
// If string contains `-` the second part is usually
// either a commit hash or things like "debian" or "alpine"
// Make sure the first part is some kind of versioning and strip the rest
if strings.Contains(tag, "-") {
split := strings.Split(tag, "-")
switch {
case semVerPattern.MatchString(split[0]):
tag = split[0]
case incompleteSemVerPattern.MatchString(split[0]):
tag = split[0]
}
}
// Re-check for incomplete SemVer after cleaning
if incompleteSemVerPattern.MatchString(tag) {
tag = cleanIncompleteSemVer(tag)
}
// Re-check for incomplete SemVer after cleaning
if incompleteSemVerPattern.MatchString(tag) {
tag = cleanIncompleteSemVer(tag)
}
if err := checkValidLabelValue(tag); err != nil {
return "", err
}
if err := checkValidLabelValue(tag); err != nil {
return "", err
}
if !semVerPattern.MatchString(tag) {
log.Warn().Msgf("Could not produce a valid SemVer tag for tag [%s]", tag)
}
if !semVerPattern.MatchString(tag) {
log.Warn().Msgf("Could not produce a valid SemVer tag for tag [%s]", tag)
}
// Build and return the updated SemVer string
return tag, nil
// Build and return the updated SemVer string
return tag, nil
}
func Clean(tag string) error {
newTag, err := CleanTag(tag)
if err != nil {
log.Fatal().Err(err).Msgf("Failed to clean tag [%s]", tag)
}
newTag, err := CleanTag(tag)
if err != nil {
log.Fatal().Err(err).Msgf("Failed to clean tag [%s]", tag)
}
log.Info().Msgf("Tag [%s] cleaned to [%s]", tag, newTag)
return nil
log.Info().Msgf("Tag [%s] cleaned to [%s]", tag, newTag)
return nil
}
func checkValidLabelValue(tag string) error {
if errs := validation.IsValidLabelValue(tag); len(errs) > 0 {
return fmt.Errorf("tag [%s] is not valid for label use. error: %s", tag, (strings.Join(errs, ", ")))
}
return nil
if errs := validation.IsValidLabelValue(tag); len(errs) > 0 {
return fmt.Errorf("tag [%s] is not valid for label use. error: %s", tag, (strings.Join(errs, ", ")))
}
return nil
}
// keepShortCommitHashSuffix keeps the last 7 characters of a tag
// eg "something-abcdefg" -> "abcdefg"
func keepShortCommitHashSuffix(tag string) string {
return strings.Split(tag, "-")[1]
return strings.Split(tag, "-")[1]
}
// cleanRelease Transforms release pattern format
// eg "RELEASE.2023-11-20T22-40-07Z" -> "2023.11.20"
func cleanRelease(tag string) string {
tag = strings.Split(tag, ".")[1]
tag = strings.Split(tag, "T")[0]
tag = strings.ReplaceAll(tag, "-", ".")
tag = strings.Split(tag, ".")[1]
tag = strings.Split(tag, "T")[0]
tag = strings.ReplaceAll(tag, "-", ".")
return tag
return tag
}
// cleanArch removes arch prefixes
// eg "x64-1.2.3" -> "1.2.3"
func cleanArch(tag string) string {
tag = strings.Split(tag, "-")[1]
tag = strings.Split(tag, "-")[1]
return tag
return tag
}
// cleanYearMonthDay Transforms date versions
// eg "2023-11-15" -> "2023.11.15" and "2022-04" -> "2022.4"
func cleanYearMonthDay(tag string) string {
tag = strings.ReplaceAll(tag, "-", ".")
parts := strings.Split(tag, ".")
for idx := range parts {
parts[idx] = strings.TrimPrefix(parts[idx], "0")
}
for len(parts) < 3 {
parts = append(parts, "0")
}
tag = strings.Join(parts, ".")
tag = strings.ReplaceAll(tag, "-", ".")
parts := strings.Split(tag, ".")
for idx := range parts {
parts[idx] = strings.TrimPrefix(parts[idx], "0")
}
for len(parts) < 3 {
parts = append(parts, "0")
}
tag = strings.Join(parts, ".")
return tag
return tag
}
// cleanIncompleteSemVer Transforms incomplete SemVer strings
// eg "1.2" -> "1.2.0" and "1" -> "1.0.0"
// versions with more parts are left as-is
func cleanIncompleteSemVer(tag string) string {
parts := strings.Split(tag, ".")
switch {
case len(parts) == 2:
tag = tag + ".0"
case len(parts) == 1:
tag = tag + ".0.0"
}
parts := strings.Split(tag, ".")
switch {
case len(parts) == 2:
tag = tag + ".0"
case len(parts) == 1:
tag = tag + ".0.0"
}
return tag
return tag
}
// cleanLeadingSymbol Trims leading 'v' or non-alphanumeric characters
// e.g "v1.2.3" -> "1.2.3"
func cleanLeadingSymbol(tag string) string {
return leadingSymbolPattern.ReplaceAllString(tag, "")
return leadingSymbolPattern.ReplaceAllString(tag, "")
}
// cleanSha Strips everything after '@'
// e.g "v1.2.3@sha256:abc123" -> "v1.2.3"
func cleanSha(tag string) string {
return strings.Split(tag, "@")[0]
return strings.Split(tag, "@")[0]
}
// cleanPrefixYearMonthDay Transforms date versions with prefix
// eg "latest-2023-12-18" -> "2023.12.18"
func cleanPrefixYearMonthDay(tag string) string {
calVer := strings.Split(tag, "-")[1:]
tag = strings.Join(calVer, ".")
calVer := strings.Split(tag, "-")[1:]
tag = strings.Join(calVer, ".")
return tag
return tag
}
+259 -259
View File
@@ -1,277 +1,277 @@
package image
import (
"strings"
"testing"
"strings"
"testing"
)
type args struct {
tag string
tag string
}
type testdata struct {
name string
args args
want string
wantErr bool
name string
args args
want string
wantErr bool
}
func TestCleanTag(t *testing.T) {
tests := []testdata{
// No match with any pattern tests
{
name: "Test valid SemVer format",
args: args{
tag: "1.2.3",
},
want: "1.2.3",
wantErr: false,
},
{
name: "Test pattern that cannot be converted to SemVer",
args: args{
tag: "latest",
},
want: "latest",
wantErr: false,
},
{
name: "Test empty tag",
args: args{
tag: "",
},
want: "",
wantErr: true,
},
{
name: "Test tag with only whitespace",
args: args{
tag: " ",
},
want: "",
wantErr: true,
},
{
name: "Test full tag with digest",
args: args{
tag: "1.2.3@sha256:abc123",
},
want: "1.2.3",
},
{
name: "Test tag with longer version and `-suffix`",
args: args{
tag: "1.2.3.4-suffix",
},
want: "1.2.3.4",
},
{
name: "Test tag with semver and `-suffix`",
args: args{
tag: "1.2.3-abc12367",
},
want: "1.2.3",
},
{
name: "Test tag with calver and `-suffix`",
args: args{
tag: "2023.11.2-abc12367",
},
want: "2023.11.2",
},
{
name: "Test with invalid label format",
args: args{
tag: strings.Repeat("a", 300),
},
want: "",
wantErr: true,
},
// cleanSha tests
{
name: "Test cleanSha",
args: args{
tag: "1.2.3@sha256:abc123",
},
want: "1.2.3",
},
// cleanLeadingSymbol tests
{
name: "Test cleanLeadingSymbol ($)",
args: args{
tag: "$1.2.3",
},
want: "1.2.3",
},
{
name: "Test cleanLeadingSymbol (v)",
args: args{
tag: "v1.2.3",
},
want: "1.2.3",
},
{
name: "Test cleanLeadingSymbol (version)",
args: args{
tag: "version-a78f38c1",
},
want: "a78f38c1",
},
// keepShortCommitHashSuffix tests
{
name: "Test keepShortCommitHashSuffix (something-hash)",
args: args{
tag: "something-abcd123",
},
want: "abcd123",
},
{
name: "Test keepShortCommitHashSuffix (version-hash)",
args: args{
tag: "version-abcd123",
},
want: "abcd123",
},
// cleanIncompleteSemVer tests
{
name: "Test cleanIncompleteSemVer (2 parts)",
args: args{
tag: "1.2",
},
want: "1.2.0",
},
{
name: "Test cleanIncompleteSemVer (1 part)",
args: args{
tag: "1",
},
want: "1.0.0",
},
{
name: "Test cleanIncompleteSemVer (more than 3 parts)",
args: args{
tag: "1.2.3.4.5",
},
want: "1.2.3.4.5",
},
{
name: "Test cleanIncompleteSemVer (with suffix)",
args: args{
tag: "2.440-jdk17",
},
want: "2.440.0",
},
// cleanYearMonthDay tests
{
name: "Test cleanYearMonthDay (year-month-day)",
args: args{
tag: "2023-11-15",
},
want: "2023.11.15",
},
{
name: "Test cleanYearMonthDay (year-month)",
args: args{
tag: "2022-04",
},
want: "2022.4.0",
},
{
name: "Test cleanYearMonthDay (with prefix)",
args: args{
tag: "latest-2023-12-18",
},
want: "2023.12.18",
},
// cleanPrefix tests
{
name: "Test cleanPrefix (random prefix)",
args: args{
tag: "abc123-v1.2.3",
},
want: "1.2.3",
},
{
name: "Test cleanPrefix (version prefix)",
args: args{
tag: "version-1.2.3",
},
want: "1.2.3",
},
// cleanArch tests
{
name: "Test cleanArch",
args: args{
tag: "x64-1.2.3",
},
want: "1.2.3",
},
// cleanRelease tests
{
name: "Test cleanRelease",
args: args{
tag: "RELEASE.2023-11-20T22-40-07Z",
},
want: "2023.11.20",
},
// cleanStupidSemVerLike tests
{
name: "Test cleanStupidSemVerLike",
args: args{
tag: "v.1.2.3",
},
want: "1.2.3",
},
{
name: "Test cleanStupidSemVerLike (2)",
args: args{
tag: "V.1.2.3",
},
want: "1.2.3",
},
}
tests := []testdata{
// No match with any pattern tests
{
name: "Test valid SemVer format",
args: args{
tag: "1.2.3",
},
want: "1.2.3",
wantErr: false,
},
{
name: "Test pattern that cannot be converted to SemVer",
args: args{
tag: "latest",
},
want: "latest",
wantErr: false,
},
{
name: "Test empty tag",
args: args{
tag: "",
},
want: "",
wantErr: true,
},
{
name: "Test tag with only whitespace",
args: args{
tag: " ",
},
want: "",
wantErr: true,
},
{
name: "Test full tag with digest",
args: args{
tag: "1.2.3@sha256:abc123",
},
want: "1.2.3",
},
{
name: "Test tag with longer version and `-suffix`",
args: args{
tag: "1.2.3.4-suffix",
},
want: "1.2.3.4",
},
{
name: "Test tag with semver and `-suffix`",
args: args{
tag: "1.2.3-abc12367",
},
want: "1.2.3",
},
{
name: "Test tag with calver and `-suffix`",
args: args{
tag: "2023.11.2-abc12367",
},
want: "2023.11.2",
},
{
name: "Test with invalid label format",
args: args{
tag: strings.Repeat("a", 300),
},
want: "",
wantErr: true,
},
// cleanSha tests
{
name: "Test cleanSha",
args: args{
tag: "1.2.3@sha256:abc123",
},
want: "1.2.3",
},
// cleanLeadingSymbol tests
{
name: "Test cleanLeadingSymbol ($)",
args: args{
tag: "$1.2.3",
},
want: "1.2.3",
},
{
name: "Test cleanLeadingSymbol (v)",
args: args{
tag: "v1.2.3",
},
want: "1.2.3",
},
{
name: "Test cleanLeadingSymbol (version)",
args: args{
tag: "version-a78f38c1",
},
want: "a78f38c1",
},
// keepShortCommitHashSuffix tests
{
name: "Test keepShortCommitHashSuffix (something-hash)",
args: args{
tag: "something-abcd123",
},
want: "abcd123",
},
{
name: "Test keepShortCommitHashSuffix (version-hash)",
args: args{
tag: "version-abcd123",
},
want: "abcd123",
},
// cleanIncompleteSemVer tests
{
name: "Test cleanIncompleteSemVer (2 parts)",
args: args{
tag: "1.2",
},
want: "1.2.0",
},
{
name: "Test cleanIncompleteSemVer (1 part)",
args: args{
tag: "1",
},
want: "1.0.0",
},
{
name: "Test cleanIncompleteSemVer (more than 3 parts)",
args: args{
tag: "1.2.3.4.5",
},
want: "1.2.3.4.5",
},
{
name: "Test cleanIncompleteSemVer (with suffix)",
args: args{
tag: "2.440-jdk17",
},
want: "2.440.0",
},
// cleanYearMonthDay tests
{
name: "Test cleanYearMonthDay (year-month-day)",
args: args{
tag: "2023-11-15",
},
want: "2023.11.15",
},
{
name: "Test cleanYearMonthDay (year-month)",
args: args{
tag: "2022-04",
},
want: "2022.4.0",
},
{
name: "Test cleanYearMonthDay (with prefix)",
args: args{
tag: "latest-2023-12-18",
},
want: "2023.12.18",
},
// cleanPrefix tests
{
name: "Test cleanPrefix (random prefix)",
args: args{
tag: "abc123-v1.2.3",
},
want: "1.2.3",
},
{
name: "Test cleanPrefix (version prefix)",
args: args{
tag: "version-1.2.3",
},
want: "1.2.3",
},
// cleanArch tests
{
name: "Test cleanArch",
args: args{
tag: "x64-1.2.3",
},
want: "1.2.3",
},
// cleanRelease tests
{
name: "Test cleanRelease",
args: args{
tag: "RELEASE.2023-11-20T22-40-07Z",
},
want: "2023.11.20",
},
// cleanStupidSemVerLike tests
{
name: "Test cleanStupidSemVerLike",
args: args{
tag: "v.1.2.3",
},
want: "1.2.3",
},
{
name: "Test cleanStupidSemVerLike (2)",
args: args{
tag: "V.1.2.3",
},
want: "1.2.3",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := CleanTag(tt.args.tag)
// If we expected an error, but didn't get one, fail the test
if (err != nil) != tt.wantErr {
t.Errorf("CleanTag() error = %v, wantErr %t", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("CleanTag() got = %v, want %v", got, tt.want)
}
})
}
got, err := CleanTag(tt.args.tag)
// If we expected an error, but didn't get one, fail the test
if (err != nil) != tt.wantErr {
t.Errorf("CleanTag() error = %v, wantErr %t", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("CleanTag() got = %v, want %v", got, tt.want)
}
})
}
}
func TestCheckValidLabelValue(t *testing.T) {
tests := []testdata{
{
name: "Test invalid label format",
args: args{
tag: "1.2.3@sha256:abc123",
},
wantErr: true,
},
{
name: "Test valid label format",
args: args{
tag: "1.2.3",
},
wantErr: false,
},
}
tests := []testdata{
{
name: "Test invalid label format",
args: args{
tag: "1.2.3@sha256:abc123",
},
wantErr: true,
},
{
name: "Test valid label format",
args: args{
tag: "1.2.3",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := checkValidLabelValue(tt.args.tag)
// If we expected an error, but didn't get one, fail the test
if (err != nil) != tt.wantErr {
t.Errorf("checkValidLabelValue() error = %v, wantErr %t", err, tt.wantErr)
return
}
})
}
err := checkValidLabelValue(tt.args.tag)
// If we expected an error, but didn't get one, fail the test
if (err != nil) != tt.wantErr {
t.Errorf("checkValidLabelValue() error = %v, wantErr %t", err, tt.wantErr)
return
}
})
}
}
+107 -107
View File
@@ -1,146 +1,146 @@
package image
import (
"fmt"
"regexp"
"strings"
"fmt"
"regexp"
"strings"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
"github.com/rs/zerolog/log"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
"github.com/rs/zerolog/log"
)
// Images represents the structure of values.yaml.
type Images struct {
ImagesMap map[string]ImageDetails
K *koanf.Koanf
ImagesMap map[string]ImageDetails
K *koanf.Koanf
}
// ImageDetails represents details for each image.
type ImageDetails struct {
Repository string `yaml:"repository"`
Tag string `yaml:"tag"`
Version string
Link string
// Add other fields as needed
Repository string `yaml:"repository"`
Tag string `yaml:"tag"`
Version string
Link string
// Add other fields as needed
}
var imageRegex = regexp.MustCompile(`^image|[a-zA-Z0-9]+Image$`)
func (i *Images) LoadValuesFile(filename string) error {
// Initialize koanf instance
i.K = koanf.New(".")
// Initialize koanf instance
i.K = koanf.New(".")
// Load YAML file using koanf
if err := i.K.Load(file.Provider(filename), yaml.Parser()); err != nil {
return err
}
// Load YAML file using koanf
if err := i.K.Load(file.Provider(filename), yaml.Parser()); err != nil {
return err
}
// List only root-level keys that match the criteria
keys := getFilteredRootLevelKeys(i.K)
i.ImagesMap = make(map[string]ImageDetails)
for _, key := range keys {
// Extract relevant fields from the loaded configuration
var img ImageDetails
if err := i.K.Unmarshal(key, &img); err != nil {
return err
}
// List only root-level keys that match the criteria
keys := getFilteredRootLevelKeys(i.K)
i.ImagesMap = make(map[string]ImageDetails)
for _, key := range keys {
// Extract relevant fields from the loaded configuration
var img ImageDetails
if err := i.K.Unmarshal(key, &img); err != nil {
return err
}
// Set the Link field based on the repository
img.Link = constructLink(img.Repository)
// Set the Link field based on the repository
img.Link = constructLink(img.Repository)
// Set the Version field based on the tag
version, err := CleanTag(img.Tag)
if err != nil {
log.Error().Err(err).Msg("❌ Failed to clean tag")
}
// Set the Version field based on the tag
version, err := CleanTag(img.Tag)
if err != nil {
log.Error().Err(err).Msg("❌ Failed to clean tag")
}
img.Version = version
img.Version = version
// Save the extracted values to the struct
i.ImagesMap[key] = img
}
// Save the extracted values to the struct
i.ImagesMap[key] = img
}
return nil
return nil
}
func getFilteredRootLevelKeys(k *koanf.Koanf) []string {
filteredKeys := []string{}
filteredKeys := []string{}
// k.Raw() returns a map[string]interface{} with all the keys and their values
// This means the keys will only be the root-level keys, we can drill into the
// values later if we want the nested keys.
for key := range k.Raw() {
if key == "imageSelector" {
log.Error().Msg("❌ Found [imageSelector] in top level keys, this is not supported.")
continue
}
// Filter keys that match the regex
if imageRegex.MatchString(key) {
filteredKeys = append(filteredKeys, key)
}
}
// k.Raw() returns a map[string]interface{} with all the keys and their values
// This means the keys will only be the root-level keys, we can drill into the
// values later if we want the nested keys.
for key := range k.Raw() {
if key == "imageSelector" {
log.Error().Msg("❌ Found [imageSelector] in top level keys, this is not supported.")
continue
}
// Filter keys that match the regex
if imageRegex.MatchString(key) {
filteredKeys = append(filteredKeys, key)
}
}
return filteredKeys
return filteredKeys
}
// constructLink constructs a link based on the repository using the logic from the main function.
func constructLink(repository string) string {
prefix := ""
prefix := ""
switch {
case strings.HasPrefix(repository, "lscr.io/linuxserver/"):
prefix = "https://fleet.linuxserver.io/image?name="
repository = strings.TrimPrefix(repository, "lscr.io/")
case strings.HasPrefix(repository, "tccr.io/tccr/"):
prefix = "https://github.com/truecharts/containers/tree/master/apps/"
repository = strings.TrimPrefix(repository, "tccr.io/tccr/")
case strings.HasPrefix(repository, "mcr.microsoft.com/"):
prefix = "https://mcr.microsoft.com/en-us/product/"
repository = strings.TrimPrefix(repository, "mcr.microsoft.com/")
case strings.HasPrefix(repository, "public.ecr.aws/"):
prefix = "https://gallery.ecr.aws/"
repository = strings.TrimPrefix(repository, "public.ecr.aws/")
case strings.HasPrefix(repository, "ghcr.io/"):
prefix = "https://"
case strings.HasPrefix(repository, "quay.io/"):
prefix = "https://"
case strings.HasPrefix(repository, "gcr.io/"):
prefix = "https://"
case strings.Contains(repository, ".azurecr.io/"):
reg := fmt.Sprintf(`%s.azurecr.io/`, strings.Split(repository, ".")[0])
prefix = fmt.Sprintf("https://%s", reg)
repository = strings.TrimPrefix(repository, reg)
case strings.Contains(repository, ".ocir.io/"):
prefix = ""
default:
// Docker Hub or unknown registry
prefix = "https://hub.docker.com/r/"
repository = strings.TrimPrefix(repository, "docker.io/")
repository = strings.TrimPrefix(repository, "index.docker.io/")
repository = strings.TrimPrefix(repository, "registry-1.docker.io/")
repository = strings.TrimPrefix(repository, "registry.hub.docker.com/")
switch {
case strings.HasPrefix(repository, "lscr.io/linuxserver/"):
prefix = "https://fleet.linuxserver.io/image?name="
repository = strings.TrimPrefix(repository, "lscr.io/")
case strings.HasPrefix(repository, "tccr.io/tccr/"):
prefix = "https://github.com/truecharts/containers/tree/master/apps/"
repository = strings.TrimPrefix(repository, "tccr.io/tccr/")
case strings.HasPrefix(repository, "mcr.microsoft.com/"):
prefix = "https://mcr.microsoft.com/en-us/product/"
repository = strings.TrimPrefix(repository, "mcr.microsoft.com/")
case strings.HasPrefix(repository, "public.ecr.aws/"):
prefix = "https://gallery.ecr.aws/"
repository = strings.TrimPrefix(repository, "public.ecr.aws/")
case strings.HasPrefix(repository, "ghcr.io/"):
prefix = "https://"
case strings.HasPrefix(repository, "quay.io/"):
prefix = "https://"
case strings.HasPrefix(repository, "gcr.io/"):
prefix = "https://"
case strings.Contains(repository, ".azurecr.io/"):
reg := fmt.Sprintf(`%s.azurecr.io/`, strings.Split(repository, ".")[0])
prefix = fmt.Sprintf("https://%s", reg)
repository = strings.TrimPrefix(repository, reg)
case strings.Contains(repository, ".ocir.io/"):
prefix = ""
default:
// Docker Hub or unknown registry
prefix = "https://hub.docker.com/r/"
repository = strings.TrimPrefix(repository, "docker.io/")
repository = strings.TrimPrefix(repository, "index.docker.io/")
repository = strings.TrimPrefix(repository, "registry-1.docker.io/")
repository = strings.TrimPrefix(repository, "registry.hub.docker.com/")
// Check for Docker Official Image
if strings.Count(repository, "/") == 0 || strings.HasPrefix(repository, "library/") {
prefix = "https://hub.docker.com/_/"
repository = strings.TrimPrefix(repository, "library/")
}
// Check for Docker Official Image
if strings.Count(repository, "/") == 0 || strings.HasPrefix(repository, "library/") {
prefix = "https://hub.docker.com/_/"
repository = strings.TrimPrefix(repository, "library/")
}
// Avoid creating a bad link if the image name has more than 1 slash
slashes := strings.Count(repository, "/")
if slashes > 1 {
prefix = ""
log.Warn().Msgf("WARNING: Could not determine source repository url for [%s]", repository)
}
}
// Avoid creating a bad link if the image name has more than 1 slash
slashes := strings.Count(repository, "/")
if slashes > 1 {
prefix = ""
log.Warn().Msgf("WARNING: Could not determine source repository url for [%s]", repository)
}
}
if prefix == "" {
log.Warn().Msgf("WARNING: Could not determine source repository url for [%s]", repository)
return ""
}
if prefix == "" {
log.Warn().Msgf("WARNING: Could not determine source repository url for [%s]", repository)
return ""
}
containerURL := fmt.Sprintf("%s%s", prefix, repository)
return containerURL
containerURL := fmt.Sprintf("%s%s", prefix, repository)
return containerURL
}
+161 -161
View File
@@ -1,171 +1,171 @@
package image
import (
"fmt"
"reflect"
"testing"
"fmt"
"reflect"
"testing"
)
func TestLoadValuesFile(t *testing.T) {
type TestData struct {
name string
valuesFile string
expected map[string]ImageDetails
wantErr bool
}
testDataPath := "../../testdata/values_yaml"
tests := []TestData{
{
name: "Test malformed file",
valuesFile: "malformedValues.yaml",
expected: nil,
wantErr: true,
},
{
name: "Test empty file",
valuesFile: "emptyValues.yaml",
expected: nil,
wantErr: false,
},
{
name: "Test single image file",
valuesFile: "singleImageValues.yaml",
expected: map[string]ImageDetails{
"image": {
Repository: "nginx",
Tag: "1.15.8",
Link: "https://hub.docker.com/_/nginx",
Version: "1.15.8",
},
},
},
{
name: "Test multiple image file",
valuesFile: "multiImageValues.yaml",
expected: map[string]ImageDetails{
"image": {
Repository: "author/image",
Tag: "1.0.0",
Link: "https://hub.docker.com/r/author/image",
Version: "1.0.0",
},
"dockerHub1Image": {
Repository: "docker.io/author/image",
Tag: "1.0.0",
Link: "https://hub.docker.com/r/author/image",
Version: "1.0.0",
},
"dockerHub2Image": {
Repository: "index.docker.io/author/image",
Tag: "1.0.0",
Link: "https://hub.docker.com/r/author/image",
Version: "1.0.0",
},
"dockerHub3Image": {
Repository: "registry-1.docker.io/author/image",
Tag: "1.0.0",
Link: "https://hub.docker.com/r/author/image",
Version: "1.0.0",
},
"dockerHub4Image": {
Repository: "registry.hub.docker.com/author/image",
Tag: "1.0.0",
Link: "https://hub.docker.com/r/author/image",
Version: "1.0.0",
},
"dockerHub5Image": {
Repository: "image",
Tag: "1.0.0",
Link: "https://hub.docker.com/_/image",
Version: "1.0.0",
},
"dockerHub6Image": {
Repository: "library/image",
Tag: "1.0.0",
Link: "https://hub.docker.com/_/image",
Version: "1.0.0",
},
"lscrImage": {
Repository: "lscr.io/linuxserver/image",
Tag: "1.0.0",
Link: "https://fleet.linuxserver.io/image?name=linuxserver/image",
Version: "1.0.0",
},
"tccrImage": {
Repository: "tccr.io/tccr/image",
Tag: "1.0.0",
Link: "https://github.com/truecharts/containers/tree/master/apps/image",
Version: "1.0.0",
},
"mcrImage": {
Repository: "mcr.microsoft.com/author/image",
Tag: "1.0.0",
Link: "https://mcr.microsoft.com/en-us/product/author/image",
Version: "1.0.0",
},
"ecrImage": {
Repository: "public.ecr.aws/author/image",
Tag: "1.0.0",
Link: "https://gallery.ecr.aws/author/image",
Version: "1.0.0",
},
"ghcrImage": {
Repository: "ghcr.io/author/image",
Tag: "1.0.0",
Link: "https://ghcr.io/author/image",
Version: "1.0.0",
},
"quayImage": {
Repository: "quay.io/author/image",
Tag: "1.0.0",
Link: "https://quay.io/author/image",
Version: "1.0.0",
},
"gcrImage": {
Repository: "gcr.io/author/image",
Tag: "1.0.0",
Link: "https://gcr.io/author/image",
Version: "1.0.0",
},
"azurecrImage": {
Repository: "author.azurecr.io/image",
Tag: "1.0.0",
Link: "https://author.azurecr.io/image",
Version: "1.0.0",
},
"ocirImage": {
Repository: "author.ocir.io/image",
Tag: "1.0.0",
Link: "",
Version: "1.0.0",
},
"unknownImage": {
Repository: "unknown.io/author/image",
Tag: "1.0.0",
Link: "",
Version: "1.0.0",
},
},
},
}
type TestData struct {
name string
valuesFile string
expected map[string]ImageDetails
wantErr bool
}
testDataPath := "../../testdata/values_yaml"
tests := []TestData{
{
name: "Test malformed file",
valuesFile: "malformedValues.yaml",
expected: nil,
wantErr: true,
},
{
name: "Test empty file",
valuesFile: "emptyValues.yaml",
expected: nil,
wantErr: false,
},
{
name: "Test single image file",
valuesFile: "singleImageValues.yaml",
expected: map[string]ImageDetails{
"image": {
Repository: "nginx",
Tag: "1.15.8",
Link: "https://hub.docker.com/_/nginx",
Version: "1.15.8",
},
},
},
{
name: "Test multiple image file",
valuesFile: "multiImageValues.yaml",
expected: map[string]ImageDetails{
"image": {
Repository: "author/image",
Tag: "1.0.0",
Link: "https://hub.docker.com/r/author/image",
Version: "1.0.0",
},
"dockerHub1Image": {
Repository: "docker.io/author/image",
Tag: "1.0.0",
Link: "https://hub.docker.com/r/author/image",
Version: "1.0.0",
},
"dockerHub2Image": {
Repository: "index.docker.io/author/image",
Tag: "1.0.0",
Link: "https://hub.docker.com/r/author/image",
Version: "1.0.0",
},
"dockerHub3Image": {
Repository: "registry-1.docker.io/author/image",
Tag: "1.0.0",
Link: "https://hub.docker.com/r/author/image",
Version: "1.0.0",
},
"dockerHub4Image": {
Repository: "registry.hub.docker.com/author/image",
Tag: "1.0.0",
Link: "https://hub.docker.com/r/author/image",
Version: "1.0.0",
},
"dockerHub5Image": {
Repository: "image",
Tag: "1.0.0",
Link: "https://hub.docker.com/_/image",
Version: "1.0.0",
},
"dockerHub6Image": {
Repository: "library/image",
Tag: "1.0.0",
Link: "https://hub.docker.com/_/image",
Version: "1.0.0",
},
"lscrImage": {
Repository: "lscr.io/linuxserver/image",
Tag: "1.0.0",
Link: "https://fleet.linuxserver.io/image?name=linuxserver/image",
Version: "1.0.0",
},
"tccrImage": {
Repository: "tccr.io/tccr/image",
Tag: "1.0.0",
Link: "https://github.com/truecharts/containers/tree/master/apps/image",
Version: "1.0.0",
},
"mcrImage": {
Repository: "mcr.microsoft.com/author/image",
Tag: "1.0.0",
Link: "https://mcr.microsoft.com/en-us/product/author/image",
Version: "1.0.0",
},
"ecrImage": {
Repository: "public.ecr.aws/author/image",
Tag: "1.0.0",
Link: "https://gallery.ecr.aws/author/image",
Version: "1.0.0",
},
"ghcrImage": {
Repository: "ghcr.io/author/image",
Tag: "1.0.0",
Link: "https://ghcr.io/author/image",
Version: "1.0.0",
},
"quayImage": {
Repository: "quay.io/author/image",
Tag: "1.0.0",
Link: "https://quay.io/author/image",
Version: "1.0.0",
},
"gcrImage": {
Repository: "gcr.io/author/image",
Tag: "1.0.0",
Link: "https://gcr.io/author/image",
Version: "1.0.0",
},
"azurecrImage": {
Repository: "author.azurecr.io/image",
Tag: "1.0.0",
Link: "https://author.azurecr.io/image",
Version: "1.0.0",
},
"ocirImage": {
Repository: "author.ocir.io/image",
Tag: "1.0.0",
Link: "",
Version: "1.0.0",
},
"unknownImage": {
Repository: "unknown.io/author/image",
Tag: "1.0.0",
Link: "",
Version: "1.0.0",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var images Images
err := images.LoadValuesFile(fmt.Sprintf("%s/%s", testDataPath, tt.valuesFile))
if (err != nil) != tt.wantErr {
t.Errorf("LoadValuesFile() error = %v, wantErr %v", err, tt.wantErr)
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var images Images
err := images.LoadValuesFile(fmt.Sprintf("%s/%s", testDataPath, tt.valuesFile))
if (err != nil) != tt.wantErr {
t.Errorf("LoadValuesFile() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.expected == nil && len(images.ImagesMap) > 0 {
t.Errorf("LoadValuesFile() expected = %+v, got %+v", tt.expected, images.ImagesMap)
}
if tt.expected == nil && len(images.ImagesMap) > 0 {
t.Errorf("LoadValuesFile() expected = %+v, got %+v", tt.expected, images.ImagesMap)
}
if tt.expected != nil {
if !reflect.DeepEqual(images.ImagesMap, tt.expected) {
t.Errorf("LoadValuesFile() expected = %+v, got %+v", tt.expected, images.ImagesMap)
}
}
})
}
if tt.expected != nil {
if !reflect.DeepEqual(images.ImagesMap, tt.expected) {
t.Errorf("LoadValuesFile() expected = %+v, got %+v", tt.expected, images.ImagesMap)
}
}
})
}
}
+40 -40
View File
@@ -1,61 +1,61 @@
package info
import (
"runtime/debug"
"time"
"runtime/debug"
"time"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog/log"
)
type Data struct {
GoVersion string
GoArch string
GoOS string
GoC bool
GitCommit string
GitDate time.Time
GitDirty bool
GoVersion string
GoArch string
GoOS string
GoC bool
GitCommit string
GitDate time.Time
GitDirty bool
}
func NewInfo() *Data {
info, _ := debug.ReadBuildInfo()
data := &Data{
GoVersion: info.GoVersion,
}
info, _ := debug.ReadBuildInfo()
data := &Data{
GoVersion: info.GoVersion,
}
// Available info: https://github.com/golang/go/blob/master/src/runtime/debug/mod.go#L73
for _, kv := range info.Settings {
switch kv.Key {
case "GOARCH":
data.GoArch = kv.Value
case "GOOS":
data.GoOS = kv.Value
case "CGO_ENABLED":
data.GoC = kv.Value == "1"
case "vcs.revision":
data.GitCommit = kv.Value
case "vcs.time":
data.GitDate, _ = time.Parse(time.RFC3339, kv.Value)
case "vcs.modified":
data.GitDirty = kv.Value == "true"
}
}
// Available info: https://github.com/golang/go/blob/master/src/runtime/debug/mod.go#L73
for _, kv := range info.Settings {
switch kv.Key {
case "GOARCH":
data.GoArch = kv.Value
case "GOOS":
data.GoOS = kv.Value
case "CGO_ENABLED":
data.GoC = kv.Value == "1"
case "vcs.revision":
data.GitCommit = kv.Value
case "vcs.time":
data.GitDate, _ = time.Parse(time.RFC3339, kv.Value)
case "vcs.modified":
data.GitDirty = kv.Value == "true"
}
}
return data
return data
}
func (d *Data) Print() {
log.Info().Msgf(`
log.Info().Msgf(`
Charttool is a tool for managing TrueCharts charts.
Go
Version: %s
OS: %s
Arch: %s
CGO: %t
Version: %s
OS: %s
Arch: %s
CGO: %t
Git
Commit: %s
Date: %s
Dirty: %t
Commit: %s
Date: %s
Dirty: %t
`, d.GoVersion, d.GoOS, d.GoArch, d.GoC, d.GitCommit, d.GitDate, d.GitDirty)
}
+23 -23
View File
@@ -1,35 +1,35 @@
package readme
import (
"fmt"
"os"
"path/filepath"
"strings"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog/log"
)
func GenerateReadme(templatePath string, chartPath string, chartName string, train string) error {
// Define file paths
template := filepath.Join(templatePath, "templates/README.md.tpl")
target := filepath.Join(filepath.Dir(chartPath), "README.md")
// Define file paths
template := filepath.Join(templatePath, "templates/README.md.tpl")
target := filepath.Join(filepath.Dir(chartPath), "README.md")
// Read template file
templateContent, err := os.ReadFile(template)
if err != nil {
return fmt.Errorf("failed to read template file: %v", err)
}
// Read template file
templateContent, err := os.ReadFile(template)
if err != nil {
return fmt.Errorf("failed to read template file: %v", err)
}
// Replace placeholders in the template
readmeContent := strings.ReplaceAll(string(templateContent), "TRAINPLACEHOLDER", train)
readmeContent = strings.ReplaceAll(readmeContent, "CHARTPLACEHOLDER", chartName)
// Replace placeholders in the template
readmeContent := strings.ReplaceAll(string(templateContent), "TRAINPLACEHOLDER", train)
readmeContent = strings.ReplaceAll(readmeContent, "CHARTPLACEHOLDER", chartName)
// Write the modified content to the README.md file in the chart directory
err = os.WriteFile(target, []byte(readmeContent), 0644)
if err != nil {
return fmt.Errorf("failed to write README.md file: %v", err)
}
// Write the modified content to the README.md file in the chart directory
err = os.WriteFile(target, []byte(readmeContent), 0644)
if err != nil {
return fmt.Errorf("failed to write README.md file: %v", err)
}
log.Info().Msgf("Generated README.md for [%s] in [%s] train", chartName, train)
return nil
log.Info().Msgf("Generated README.md for [%s] in [%s] train", chartName, train)
return nil
}
@@ -2,59 +2,59 @@ package valuesYaml
// Addons represents the schema for the 'addons' section.
type Addons struct {
Codeserver Codeserver `yaml:"codeserver,omitempty" schema:"additional_attrs:true,type:dict"`
Netshoot Netshoot `yaml:"netshoot,omitempty" schema:"additional_attrs:true,type:dict"`
VPN VPN `yaml:"vpn,omitempty" schema:"additional_attrs:true,type:dict"`
Codeserver Codeserver `yaml:"codeserver,omitempty" schema:"additional_attrs:true,type:dict"`
Netshoot Netshoot `yaml:"netshoot,omitempty" schema:"additional_attrs:true,type:dict"`
VPN VPN `yaml:"vpn,omitempty" schema:"additional_attrs:true,type:dict"`
}
// Codeserver represents the schema for the 'codeserver' addon.
type Codeserver struct {
Enabled bool `yaml:"enabled" schema:"type:boolean,default:false,show_subquestions_if:true"`
Service ServiceConfiguration `yaml:"service,omitempty" schema:"additional_attrs:true,type:dict"`
Ingress IngressConfiguration `yaml:"ingress,omitempty" schema:"additional_attrs:true,type:dict"`
EnvList []EnvList `yaml:"envList,omitempty" schema:"type:list,default:[],items:type:dict,show_if:[[type,!=,disabled]]"`
Enabled bool `yaml:"enabled" schema:"type:boolean,default:false,show_subquestions_if:true"`
Service ServiceConfiguration `yaml:"service,omitempty" schema:"additional_attrs:true,type:dict"`
Ingress IngressConfiguration `yaml:"ingress,omitempty" schema:"additional_attrs:true,type:dict"`
EnvList []EnvList `yaml:"envList,omitempty" schema:"type:list,default:[],items:type:dict,show_if:[[type,!=,disabled]]"`
}
// Netshoot represents the schema for the 'netshoot' addon.
type Netshoot struct {
Enabled bool `yaml:"enabled" schema:"type:boolean,default:false,show_subquestions_if:true"`
EnvList []EnvList `yaml:"envList,omitempty" schema:"type:list,default:[],items:type:dict,show_if:[['type','!=','disabled']]"`
Enabled bool `yaml:"enabled" schema:"type:boolean,default:false,show_subquestions_if:true"`
EnvList []EnvList `yaml:"envList,omitempty" schema:"type:list,default:[],items:type:dict,show_if:[['type','!=','disabled']]"`
}
// VPN represents the schema for the 'vpn' addon.
type VPN struct {
Type string `yaml:"type,omitempty" schema:"type:string,default:disabled,enum:,disabled,gluetun,tailscale,openvpn,wireguard"`
OpenVPN OpenVPNSettings `yaml:"openvpn,omitempty" schema:"additional_attrs:true,type:dict,show_if:[[type,=,openvpn]]"`
Tailscale TailscaleSettings `yaml:"tailscale,omitempty" schema:"additional_attrs:true,type:dict,show_if:[[type,=,tailscale]]"`
KillSwitch bool `yaml:"killSwitch" schema:"type:boolean,show_if:[[type,!=,disabled]],default:true"`
ExcludedNetworksIPv4 []ExcludedNetwork `yaml:"excludedNetworks_IPv4,omitempty" schema:"type:list,default:[],items:type:dict,show_if:[[type,!=,disabled]]"`
ExcludedNetworksIPv6 []ExcludedNetwork `yaml:"excludedNetworks_IPv6,omitempty" schema:"type:list,default:[],items:type:dict,show_if:[[type,!=,disabled]]"`
ConfigFile string `yaml:"configFile,omitempty" schema:"type:string,show_if:[[type,!=,disabled]]"`
EnvList []EnvList `yaml:"envList,omitempty" schema:"type:list,default:[],items:type:dict,show_if:[['type','!=','disabled']]"`
Type string `yaml:"type,omitempty" schema:"type:string,default:disabled,enum:,disabled,gluetun,tailscale,openvpn,wireguard"`
OpenVPN OpenVPNSettings `yaml:"openvpn,omitempty" schema:"additional_attrs:true,type:dict,show_if:[[type,=,openvpn]]"`
Tailscale TailscaleSettings `yaml:"tailscale,omitempty" schema:"additional_attrs:true,type:dict,show_if:[[type,=,tailscale]]"`
KillSwitch bool `yaml:"killSwitch" schema:"type:boolean,show_if:[[type,!=,disabled]],default:true"`
ExcludedNetworksIPv4 []ExcludedNetwork `yaml:"excludedNetworks_IPv4,omitempty" schema:"type:list,default:[],items:type:dict,show_if:[[type,!=,disabled]]"`
ExcludedNetworksIPv6 []ExcludedNetwork `yaml:"excludedNetworks_IPv6,omitempty" schema:"type:list,default:[],items:type:dict,show_if:[[type,!=,disabled]]"`
ConfigFile string `yaml:"configFile,omitempty" schema:"type:string,show_if:[[type,!=,disabled]]"`
EnvList []EnvList `yaml:"envList,omitempty" schema:"type:list,default:[],items:type:dict,show_if:[['type','!=','disabled']]"`
}
// OpenVPNSettings represents the schema for OpenVPN settings.
type OpenVPNSettings struct {
Username string `yaml:"username,omitempty" schema:"type:string,default:''"`
Password string `yaml:"password,omitempty" schema:"type:string,show_if:[[username,!=,]],default:''"`
Username string `yaml:"username,omitempty" schema:"type:string,default:''"`
Password string `yaml:"password,omitempty" schema:"type:string,show_if:[[username,!=,]],default:''"`
}
// TailscaleSettings represents the schema for Tailscale settings.
type TailscaleSettings struct {
AuthKey string `yaml:"authkey,omitempty" schema:"type:string,private:true,default:''"`
AuthOnce bool `yaml:"auth_once,omitempty" schema:"type:boolean,default:true"`
AcceptDNS bool `yaml:"accept_dns,omitempty" schema:"type:boolean,default:false"`
Userspace bool `yaml:"userspace,omitempty" schema:"type:boolean,default:false"`
Routes string `yaml:"routes,omitempty" schema:"type:string,default:''"`
DestIP string `yaml:"dest_ip,omitempty" schema:"type:string,default:''"`
Sock5Server string `yaml:"sock5_server,omitempty" schema:"type:string,default:''"`
OutboundHTTPProxyListen string `yaml:"outbound_http_proxy_listen,omitempty" schema:"type:string,default:''"`
ExtraArgs string `yaml:"extra_args,omitempty" schema:"type:string,default:''"`
DaemonExtraArgs string `yaml:"daemon_extra_args,omitempty" schema:"type:string,default:''"`
AuthKey string `yaml:"authkey,omitempty" schema:"type:string,private:true,default:''"`
AuthOnce bool `yaml:"auth_once,omitempty" schema:"type:boolean,default:true"`
AcceptDNS bool `yaml:"accept_dns,omitempty" schema:"type:boolean,default:false"`
Userspace bool `yaml:"userspace,omitempty" schema:"type:boolean,default:false"`
Routes string `yaml:"routes,omitempty" schema:"type:string,default:''"`
DestIP string `yaml:"dest_ip,omitempty" schema:"type:string,default:''"`
Sock5Server string `yaml:"sock5_server,omitempty" schema:"type:string,default:''"`
OutboundHTTPProxyListen string `yaml:"outbound_http_proxy_listen,omitempty" schema:"type:string,default:''"`
ExtraArgs string `yaml:"extra_args,omitempty" schema:"type:string,default:''"`
DaemonExtraArgs string `yaml:"daemon_extra_args,omitempty" schema:"type:string,default:''"`
}
// ExcludedNetwork represents the schema for an excluded network in the killswitch.
type ExcludedNetwork struct {
NetworkV4 string `yaml:"networkv4,omitempty" schema:"type:string,required:true"`
NetworkV6 string `yaml:"networkv6,omitempty" schema:"type:string,required:true"`
NetworkV4 string `yaml:"networkv4,omitempty" schema:"type:string,required:true"`
NetworkV6 string `yaml:"networkv6,omitempty" schema:"type:string,required:true"`
}
@@ -2,35 +2,35 @@ package valuesYaml
// InterfaceConfiguration represents the configuration for an interface.
type InterfaceConfiguration struct {
HostInterface string `yaml:"hostInterface,omitempty" schema:"type:string" required:"true" description:"Host Interface"`
HostInterface string `yaml:"hostInterface,omitempty" schema:"type:string" required:"true" description:"Host Interface"`
}
// IPAMConfiguration represents the configuration for IP Address Management.
type IPAMConfiguration struct {
Type string `yaml:"type,omitempty" schema:"type:string" required:"true" enum:"[dhcp, static]" description:"IPAM Type"`
StaticIPConfigurations []string `yaml:"staticIPConfigurations,omitempty" schema:"type:list" show_if:"[['type', '=', 'static']]" items:"type:ipaddr,cidr:true" description:"Static IP Addresses"`
StaticRoutes []StaticRouteConfiguration `yaml:"staticRoutes,omitempty" schema:"type:list" show_if:"[['type', '=', 'static']]" description:"Static Routes"`
Type string `yaml:"type,omitempty" schema:"type:string" required:"true" enum:"[dhcp, static]" description:"IPAM Type"`
StaticIPConfigurations []string `yaml:"staticIPConfigurations,omitempty" schema:"type:list" show_if:"[['type', '=', 'static']]" items:"type:ipaddr,cidr:true" description:"Static IP Addresses"`
StaticRoutes []StaticRouteConfiguration `yaml:"staticRoutes,omitempty" schema:"type:list" show_if:"[['type', '=', 'static']]" description:"Static Routes"`
}
// StaticRouteConfiguration represents the configuration for a static route.
type StaticRouteConfiguration struct {
Destination string `yaml:"destination,omitempty" schema:"type:ipaddr,cidr:true" required:"true" description:"Destination"`
Gateway string `yaml:"gateway,omitempty" schema:"type:ipaddr,cidr:false" required:"true" description:"Gateway"`
Destination string `yaml:"destination,omitempty" schema:"type:ipaddr,cidr:true" required:"true" description:"Destination"`
Gateway string `yaml:"gateway,omitempty" schema:"type:ipaddr,cidr:false" required:"true" description:"Gateway"`
}
// NetworkingExpertConfiguration represents the expert configuration for networking.
type NetworkingExpertConfiguration struct {
ScaleExternalInterface bool `yaml:"scaleExternalInterface,omitempty" schema:"type:boolean" default:"false" show_subquestions_if:"true" description:"Add External Interfaces"`
InterfaceConfiguration InterfaceConfiguration `yaml:"interfaceConfiguration,omitempty" schema:"type:dict" $ref:"normalize/interfaceConfiguration" description:"Interface Configuration"`
IPAM IPAMConfiguration `yaml:"ipam,omitempty" schema:"type:dict" required:"true" description:"IP Address Management"`
ScaleExternalInterface bool `yaml:"scaleExternalInterface,omitempty" schema:"type:boolean" default:"false" show_subquestions_if:"true" description:"Add External Interfaces"`
InterfaceConfiguration InterfaceConfiguration `yaml:"interfaceConfiguration,omitempty" schema:"type:dict" $ref:"normalize/interfaceConfiguration" description:"Interface Configuration"`
IPAM IPAMConfiguration `yaml:"ipam,omitempty" schema:"type:dict" required:"true" description:"IP Address Management"`
}
// ServiceExpertConfiguration represents the expert configuration for services.
type ServiceExpertConfiguration struct {
ScaleExternalInterface bool `yaml:"scaleExternalInterface,omitempty" schema:"type:boolean" default:"false" show_subquestions_if:"true" description:"Add External Interfaces"`
ScaleExternalInterface bool `yaml:"scaleExternalInterface,omitempty" schema:"type:boolean" default:"false" show_subquestions_if:"true" description:"Add External Interfaces"`
}
// NetworkingConfiguration represents the configuration for networking.
type NetworkingConfiguration struct {
ExpertConfiguration NetworkingExpertConfiguration `yaml:"expertConfiguration,omitempty" schema:"type:boolean" default:"false" show_subquestions_if:"true" description:"Show Expert Config"`
ExpertConfiguration NetworkingExpertConfiguration `yaml:"expertConfiguration,omitempty" schema:"type:boolean" default:"false" show_subquestions_if:"true" description:"Show Expert Config"`
}
@@ -2,58 +2,58 @@ package valuesYaml
// MiddlewareEntry represents the schema for a middleware entry.
type MiddlewareEntry struct {
Name string `yaml:"name" schema:"type:string,default:'',required:true"`
Name string `yaml:"name" schema:"type:string,default:'',required:true"`
}
// Integration represents the schema for integrations.
type Integration struct {
Homepage IntegrationHomepage `yaml:"homepage" schema:"additional_attrs:true,type:dict"`
Homepage IntegrationHomepage `yaml:"homepage" schema:"additional_attrs:true,type:dict"`
}
// IntegrationHomepage represents the schema for the Homepage integration.
type IntegrationHomepage struct {
Enabled bool `yaml:"enabled" schema:"type:boolean,default:false"`
Name string `yaml:"name" schema:"type:string,default:'',show_if:[[enabled,=,true]]"`
Description string `yaml:"description" schema:"type:string,default:'',show_if:[[enabled,=,true]]"`
Group string `yaml:"group" schema:"type:string,default:default,show_if:[[enabled,=,true]]"`
Enabled bool `yaml:"enabled" schema:"type:boolean,default:false"`
Name string `yaml:"name" schema:"type:string,default:'',show_if:[[enabled,=,true]]"`
Description string `yaml:"description" schema:"type:string,default:'',show_if:[[enabled,=,true]]"`
Group string `yaml:"group" schema:"type:string,default:default,show_if:[[enabled,=,true]]"`
}
// TLSEntry represents the schema for a TLS entry.
type TLSEntry struct {
Host []string `yaml:"hosts" schema:"type:list,default:[],items:type:string,required:true"`
CertificateIssuer string `yaml:"certificateIssuer" schema:"type:string,default:''"`
ClusterCertificate string `yaml:"clusterCertificate" schema:"type:string,show_if:[[certificateIssuer,=,]]"`
SecretName string `yaml:"secretName" schema:"type:string,show_if:[[certificateIssuer,=,]]"`
ScaleCert int `yaml:"scaleCert" schema:"type:int,show_if:[[certificateIssuer,=,]]"`
Host []string `yaml:"hosts" schema:"type:list,default:[],items:type:string,required:true"`
CertificateIssuer string `yaml:"certificateIssuer" schema:"type:string,default:''"`
ClusterCertificate string `yaml:"clusterCertificate" schema:"type:string,show_if:[[certificateIssuer,=,]]"`
SecretName string `yaml:"secretName" schema:"type:string,show_if:[[certificateIssuer,=,]]"`
ScaleCert int `yaml:"scaleCert" schema:"type:int,show_if:[[certificateIssuer,=,]]"`
}
// IngressConfiguration represents the schema for Ingress settings with variable name.
type IngressConfiguration struct {
Enabled bool `yaml:"enabled" schema:"type:boolean,default:true,hidden:true"`
Name string `yaml:"name" schema:"type:string,default:''"`
IngressClassName string `yaml:"ingressClassName" schema:"type:string,default:''"`
AllowCors bool `yaml:"allowCors" schema:"type:boolean,show_if:[[advanced,=,true]],default:false"`
Hosts []HostEntry `yaml:"hosts" schema:"type:list,default:[],items:type:dict"`
CertificateIssuer string `yaml:"certificateIssuer" schema:"type:string,default:''"`
TLS []TLSEntry `yaml:"tls" schema:"type:list,default:[],items:type:dict,show_if:[[certificateIssuer,=,]]"`
Integration Integration `yaml:"integration" schema:"additional_attrs:true,type:dict"`
Entrypoint string `yaml:"entrypoint" schema:"type:string,default:websecure,required:true"`
Middlewares []MiddlewareEntry `yaml:"middlewares" schema:"type:list,default:[],items:type:dict"`
Enabled bool `yaml:"enabled" schema:"type:boolean,default:true,hidden:true"`
Name string `yaml:"name" schema:"type:string,default:''"`
IngressClassName string `yaml:"ingressClassName" schema:"type:string,default:''"`
AllowCors bool `yaml:"allowCors" schema:"type:boolean,show_if:[[advanced,=,true]],default:false"`
Hosts []HostEntry `yaml:"hosts" schema:"type:list,default:[],items:type:dict"`
CertificateIssuer string `yaml:"certificateIssuer" schema:"type:string,default:''"`
TLS []TLSEntry `yaml:"tls" schema:"type:list,default:[],items:type:dict,show_if:[[certificateIssuer,=,]]"`
Integration Integration `yaml:"integration" schema:"additional_attrs:true,type:dict"`
Entrypoint string `yaml:"entrypoint" schema:"type:string,default:websecure,required:true"`
Middlewares []MiddlewareEntry `yaml:"middlewares" schema:"type:list,default:[],items:type:dict"`
}
// HostEntry represents the schema for a host entry.
type HostEntry struct {
Host string `yaml:"host" schema:"type:string,default:'',required:true"`
Paths []PathEntry `yaml:"paths" schema:"type:list,default:[{path:/,pathType:Prefix}],items:type:dict"`
Host string `yaml:"host" schema:"type:string,default:'',required:true"`
Paths []PathEntry `yaml:"paths" schema:"type:list,default:[{path:/,pathType:Prefix}],items:type:dict"`
}
// PathEntry represents the schema for a path entry.
type PathEntry struct {
Path string `yaml:"path" schema:"type:string,required:true,default:/"`
PathType string `yaml:"pathType" schema:"type:string,required:true,default:Prefix"`
Path string `yaml:"path" schema:"type:string,required:true,default:/"`
PathType string `yaml:"pathType" schema:"type:string,required:true,default:Prefix"`
}
// RootReference represents the root-level reference for Ingress settings.
type RootReference struct {
Ingress map[string]IngressConfiguration `yaml:"ingress" schema:"additional_attrs:true,type:dict" description:"Ingress Settings"`
Ingress map[string]IngressConfiguration `yaml:"ingress" schema:"additional_attrs:true,type:dict" description:"Ingress Settings"`
}
@@ -2,15 +2,15 @@ package valuesYaml
// PrometheusRule represents the schema for Prometheus rule settings.
type PrometheusRule struct {
Enabled bool `yaml:"enabled" schema:"type:boolean,default:false"`
Enabled bool `yaml:"enabled" schema:"type:boolean,default:false"`
}
// MetricsConfiguration represents the metrics configuration.
type MetricsConfiguration struct {
Enabled bool `yaml:"enabled"`
PrometheusRule struct {
Enabled bool `yaml:"enabled"`
// ... other prometheusRule configuration
} `yaml:"prometheusRule"`
// ... other metrics configuration
Enabled bool `yaml:"enabled"`
PrometheusRule struct {
Enabled bool `yaml:"enabled"`
// ... other prometheusRule configuration
} `yaml:"prometheusRule"`
// ... other metrics configuration
}
@@ -2,79 +2,79 @@ package valuesYaml
// NetworkPolicyEntry represents the schema for a network policy entry.
type NetworkPolicyEntry struct {
Name string `yaml:"name" schema:"type:string,required:true,default:''"`
Enabled bool `yaml:"enabled" schema:"type:boolean,default:false,show_subquestions_if:true"`
Policy string `yaml:"policyType" schema:"type:string,default:'',enum:,ingress,egress,ingress-egress"`
Ingress []IngressEntry `yaml:"ingress" schema:"type:list,default:[],items:type:dict"`
Egress []EgressEntry `yaml:"egress" schema:"type:list,default:[],items:type:dict"`
Name string `yaml:"name" schema:"type:string,required:true,default:''"`
Enabled bool `yaml:"enabled" schema:"type:boolean,default:false,show_subquestions_if:true"`
Policy string `yaml:"policyType" schema:"type:string,default:'',enum:,ingress,egress,ingress-egress"`
Ingress []IngressEntry `yaml:"ingress" schema:"type:list,default:[],items:type:dict"`
Egress []EgressEntry `yaml:"egress" schema:"type:list,default:[],items:type:dict"`
}
// IngressEntry represents the schema for an ingress entry.
type IngressEntry struct {
From []FromEntry `yaml:"from" schema:"type:list,default:[],items:type:dict"`
NamespaceSel NamespaceSel `yaml:"namespaceSelector" schema:"additional_attrs:true,type:dict"`
PodSel PodSel `yaml:"podSelector" schema:"additional_attrs:true,type:dict"`
Ports []PortsEntry `yaml:"ports" schema:"type:list,default:[],items:type:dict"`
From []FromEntry `yaml:"from" schema:"type:list,default:[],items:type:dict"`
NamespaceSel NamespaceSel `yaml:"namespaceSelector" schema:"additional_attrs:true,type:dict"`
PodSel PodSel `yaml:"podSelector" schema:"additional_attrs:true,type:dict"`
Ports []PortsEntry `yaml:"ports" schema:"type:list,default:[],items:type:dict"`
}
// EgressEntry represents the schema for an egress entry.
type EgressEntry struct {
To []ToEntry `yaml:"to" schema:"type:list,default:[],items:type:dict"`
NamespaceSel NamespaceSel `yaml:"namespaceSelector" schema:"additional_attrs:true,type:dict"`
PodSel PodSel `yaml:"podSelector" schema:"additional_attrs:true,type:dict"`
Ports []PortsEntry `yaml:"ports" schema:"type:list,default:[],items:type:dict"`
To []ToEntry `yaml:"to" schema:"type:list,default:[],items:type:dict"`
NamespaceSel NamespaceSel `yaml:"namespaceSelector" schema:"additional_attrs:true,type:dict"`
PodSel PodSel `yaml:"podSelector" schema:"additional_attrs:true,type:dict"`
Ports []PortsEntry `yaml:"ports" schema:"type:list,default:[],items:type:dict"`
}
// FromEntry represents the schema for a 'from' entry.
type FromEntry struct {
IPBlock IPBlock `yaml:"ipBlock" schema:"additional_attrs:true,type:dict"`
NamespaceSel NamespaceSel `yaml:"namespaceSelector" schema:"additional_attrs:true,type:dict"`
PodSel PodSel `yaml:"podSelector" schema:"additional_attrs:true,type:dict"`
IPBlock IPBlock `yaml:"ipBlock" schema:"additional_attrs:true,type:dict"`
NamespaceSel NamespaceSel `yaml:"namespaceSelector" schema:"additional_attrs:true,type:dict"`
PodSel PodSel `yaml:"podSelector" schema:"additional_attrs:true,type:dict"`
}
// ToEntry represents the schema for a 'to' entry.
type ToEntry struct {
IPBlock IPBlock `yaml:"ipBlock" schema:"additional_attrs:true,type:dict"`
NamespaceSel NamespaceSel `yaml:"namespaceSelector" schema:"additional_attrs:true,type:dict"`
PodSel PodSel `yaml:"podSelector" schema:"additional_attrs:true,type:dict"`
IPBlock IPBlock `yaml:"ipBlock" schema:"additional_attrs:true,type:dict"`
NamespaceSel NamespaceSel `yaml:"namespaceSelector" schema:"additional_attrs:true,type:dict"`
PodSel PodSel `yaml:"podSelector" schema:"additional_attrs:true,type:dict"`
}
// IPBlock represents the schema for an IP block.
type IPBlock struct {
CIDR string `yaml:"cidr" schema:"type:string,default:''"`
Except []Except `yaml:"except" schema:"type:list,default:[],items:type:dict"`
CIDR string `yaml:"cidr" schema:"type:string,default:''"`
Except []Except `yaml:"except" schema:"type:list,default:[],items:type:dict"`
}
// Except represents the schema for the 'except' field.
type Except struct {
ExceptInt string `yaml:"exceptint" schema:"type:string"`
ExceptInt string `yaml:"exceptint" schema:"type:string"`
}
// NamespaceSel represents the schema for namespace selector.
type NamespaceSel struct {
MatchExpressions []ExpressionEntry `yaml:"matchExpressions" schema:"type:list,default:[],items:type:dict"`
MatchExpressions []ExpressionEntry `yaml:"matchExpressions" schema:"type:list,default:[],items:type:dict"`
}
// PodSel represents the schema for pod selector.
type PodSel struct {
MatchExpressions []ExpressionEntry `yaml:"matchExpressions" schema:"type:list,default:[],items:type:dict"`
MatchExpressions []ExpressionEntry `yaml:"matchExpressions" schema:"type:list,default:[],items:type:dict"`
}
// ExpressionEntry represents the schema for an expression entry.
type ExpressionEntry struct {
Key string `yaml:"key" schema:"type:string"`
Operator string `yaml:"operator" schema:"type:string,default:TCP,enum:TCP,UDP,SCTP,In,NotIn,Exists,DoesNotExist"`
Values []Value `yaml:"values" schema:"type:list,default:[],items:type:dict"`
Key string `yaml:"key" schema:"type:string"`
Operator string `yaml:"operator" schema:"type:string,default:TCP,enum:TCP,UDP,SCTP,In,NotIn,Exists,DoesNotExist"`
Values []Value `yaml:"values" schema:"type:list,default:[],items:type:dict"`
}
// Value represents the schema for a value entry.
type Value struct {
Value string `yaml:"value" schema:"type:string"`
Value string `yaml:"value" schema:"type:string"`
}
// PortsEntry represents the schema for a ports entry.
type PortsEntry struct {
Port int `yaml:"port" schema:"type:int"`
EndPort int `yaml:"endPort" schema:"type:int"`
Protocol string `yaml:"protocol" schema:"type:string,default:TCP,enum:TCP,UDP,SCTP"`
Port int `yaml:"port" schema:"type:int"`
EndPort int `yaml:"endPort" schema:"type:int"`
Protocol string `yaml:"protocol" schema:"type:string,default:TCP,enum:TCP,UDP,SCTP"`
}
+66 -66
View File
@@ -1,12 +1,12 @@
package valuesYaml
import (
"fmt"
"os"
"fmt"
"os"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
)
// TODO: enable validation
@@ -14,100 +14,100 @@ import (
// GlobalSettings represents the "Global Settings" section of the schema.
type GlobalSettings struct {
StopAll bool `yaml:"stopAll" description:"Stops All Running pods and hibernates cnpg" default:"false"`
StopAll bool `yaml:"stopAll" description:"Stops All Running pods and hibernates cnpg" default:"false"`
}
// ImagePullSecretEntry represents an entry in the Image Pull Secrets configuration.
type ImagePullSecretEntry struct {
Registry string `yaml:"registry" validate:"required" description:"Registry"`
Username string `yaml:"username" validate:"required" description:"Username"`
Password string `yaml:"password" validate:"required" description:"Password"`
Email string `yaml:"email" validate:"required" description:"Email"`
Registry string `yaml:"registry" validate:"required" description:"Registry"`
Username string `yaml:"username" validate:"required" description:"Username"`
Password string `yaml:"password" validate:"required" description:"Password"`
Email string `yaml:"email" validate:"required" description:"Email"`
}
// Values represents the entire configuration.
type Values struct {
Global GlobalSettings `yaml:"global"`
Workload map[string]WorkloadRootReference `yaml:"workload,omitempty" schema:"additional_attrs:true,type:dict"`
ImagePullSecretList []ImagePullSecretEntry `yaml:"imagePullSecretList,omitempty" schema:"type:list" items:"type:dict" additional_attrs:"true" description:"Image Pull Secrets"`
PodOptions PodOptions `yaml:"podOptions,omitempty" description:"Global Pod Options (Advanced)"`
Service map[string]ServiceConfiguration `yaml:"service,omitempty" schema:"additional_attrs:true,type:dict" description:"Service Settings"`
ServiceExpert ServiceExpertConfiguration `yaml:"serviceexpert,omitempty" schema:"type:boolean" default:"false" show_subquestions_if:"true" description:"Show Expert Config"`
ServiceList []ServiceConfiguration `yaml:"serviceList,omitempty" schema:"type:list,default:[]" items:"type:dict" description:"Add Manual Custom Services"`
Persistence map[string]Persistence `yaml:"persistence,omitempty" schema:"type:dict" description:"Integrated Persistent Storage"`
PersistenceList []Persistence `yaml:"persistenceList,omitempty" schema:"type:list,default:[]" description:"Additional App Storage"`
Ingress map[string]IngressConfiguration `yaml:"ingress,omitempty" schema:"additional_attrs:true,type:dict" description:"Ingress Settings"`
IngressList []IngressConfiguration `yaml:"ingressList,omitempty" schema:"type:list,default:[]"`
SecurityContext SecurityContext `yaml:"securityContext,omitempty" schema:"additional_attrs:true,type:dict"`
Resources Resources `yaml:"resources,omitempty" schema:"additional_attrs:true,type:dict"`
DeviceList DeviceList `yaml:"deviceList,omitempty" schema:"type:list,default:[]"`
ScaleGPU ScaleGPU `yaml:"scaleGPU,omitempty" schema:"type:list,default:[]"`
Metrics map[string]MetricsConfiguration `yaml:"metrics,omitempty" schema:"additional_attrs:true,type:dict"`
NetworkPolicy []NetworkPolicyEntry `yaml:"networkPolicy,omitempty" schema:"type:list,default:[]"`
Addons Addons `yaml:"addons,omitempty" schema:"additional_attrs:true,type:dict"`
Global GlobalSettings `yaml:"global"`
Workload map[string]WorkloadRootReference `yaml:"workload,omitempty" schema:"additional_attrs:true,type:dict"`
ImagePullSecretList []ImagePullSecretEntry `yaml:"imagePullSecretList,omitempty" schema:"type:list" items:"type:dict" additional_attrs:"true" description:"Image Pull Secrets"`
PodOptions PodOptions `yaml:"podOptions,omitempty" description:"Global Pod Options (Advanced)"`
Service map[string]ServiceConfiguration `yaml:"service,omitempty" schema:"additional_attrs:true,type:dict" description:"Service Settings"`
ServiceExpert ServiceExpertConfiguration `yaml:"serviceexpert,omitempty" schema:"type:boolean" default:"false" show_subquestions_if:"true" description:"Show Expert Config"`
ServiceList []ServiceConfiguration `yaml:"serviceList,omitempty" schema:"type:list,default:[]" items:"type:dict" description:"Add Manual Custom Services"`
Persistence map[string]Persistence `yaml:"persistence,omitempty" schema:"type:dict" description:"Integrated Persistent Storage"`
PersistenceList []Persistence `yaml:"persistenceList,omitempty" schema:"type:list,default:[]" description:"Additional App Storage"`
Ingress map[string]IngressConfiguration `yaml:"ingress,omitempty" schema:"additional_attrs:true,type:dict" description:"Ingress Settings"`
IngressList []IngressConfiguration `yaml:"ingressList,omitempty" schema:"type:list,default:[]"`
SecurityContext SecurityContext `yaml:"securityContext,omitempty" schema:"additional_attrs:true,type:dict"`
Resources Resources `yaml:"resources,omitempty" schema:"additional_attrs:true,type:dict"`
DeviceList DeviceList `yaml:"deviceList,omitempty" schema:"type:list,default:[]"`
ScaleGPU ScaleGPU `yaml:"scaleGPU,omitempty" schema:"type:list,default:[]"`
Metrics map[string]MetricsConfiguration `yaml:"metrics,omitempty" schema:"additional_attrs:true,type:dict"`
NetworkPolicy []NetworkPolicyEntry `yaml:"networkPolicy,omitempty" schema:"type:list,default:[]"`
Addons Addons `yaml:"addons,omitempty" schema:"additional_attrs:true,type:dict"`
}
// ValuesFile represents the entire values.yaml structure.
type ValuesFile struct {
K *koanf.Koanf
Values Values `yaml:"metadata" validate:"required,dive"`
K *koanf.Koanf
Values Values `yaml:"metadata" validate:"required,dive"`
}
func NewValuesFile() *ValuesFile {
return &ValuesFile{
K: koanf.New("."),
}
return &ValuesFile{
K: koanf.New("."),
}
}
// LoadFromFile loads values from a YAML file into the Helmvalues struct.
func (v *ValuesFile) LoadFromFile(filename string) error {
if v.K == nil {
v.K = koanf.New(".")
}
if v.K == nil {
v.K = koanf.New(".")
}
if err := v.K.Load(file.Provider(filename), yaml.Parser()); err != nil {
return fmt.Errorf("error loading from file %s: %v", filename, err)
}
if err := v.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 values struct
if err := v.K.Unmarshal("", &v.Values); err != nil {
return fmt.Errorf("error unmarshalling data: %v", err)
}
// Unmarshal the data into the values struct
if err := v.K.Unmarshal("", &v.Values); err != nil {
return fmt.Errorf("error unmarshalling data: %v", err)
}
// NOTE: Can be uncommented for debugging
// loadedData, _ := v.K.Marshal(yaml.Parser())
// log.Info().Msgf("Loaded struct data:\n%s\n", loadedData)
// NOTE: Can be uncommented for debugging
// loadedData, _ := v.K.Marshal(yaml.Parser())
// log.Info().Msgf("Loaded struct data:\n%s\n", loadedData)
// Set default values for fields if they are not set or empty
v.setDefaultValues()
// Set default values for fields if they are not set or empty
v.setDefaultValues()
// TODO: enable validation
// Validate the loaded data
// if err := validate.Struct(v.Values); err != nil {
// return fmt.Errorf("values.yaml validation error: %v", err)
// }
// TODO: enable validation
// Validate the loaded data
// if err := validate.Struct(v.Values); err != nil {
// return fmt.Errorf("values.yaml validation error: %v", err)
// }
return nil
return nil
}
// setDefaultValues sets default values for fields in valuesMetadata if they are not set or empty.
func (v *ValuesFile) setDefaultValues() {
// Set default values for other fields as needed
// Set default values for other fields as needed
}
// SaveToFile saves the Helm values metadata back to the values.yaml file.
func (v *ValuesFile) SaveToFile(filename string) error {
// Marshal the existing metadata to YAML
loadedData, err := v.K.Marshal(yaml.Parser())
if err != nil {
return fmt.Errorf("error marshalling data: %v", err)
}
// Marshal the existing metadata to YAML
loadedData, err := v.K.Marshal(yaml.Parser())
if err != nil {
return fmt.Errorf("error marshalling data: %v", err)
}
// Write the configuration to the file using os.WriteFile
err = os.WriteFile(filename, loadedData, 0644)
if err != nil {
return fmt.Errorf("error writing to file %s: %v", filename, err)
}
// Write the configuration to the file using os.WriteFile
err = os.WriteFile(filename, loadedData, 0644)
if err != nil {
return fmt.Errorf("error writing to file %s: %v", filename, err)
}
return nil
return nil
}
@@ -2,73 +2,73 @@ package valuesYaml
// ISCSIOptions represents the schema for iSCSI Options.
type ISCSIOptions struct {
TargetPortal string `yaml:"targetPortal" schema:"type:string,required:true" description:"targetPortal"`
IQN string `yaml:"iqn" schema:"type:string,required:true" description:"iqn"`
LUN int `yaml:"lun" schema:"type:int,default:0" description:"lun"`
AuthSession AuthSession `yaml:"authSession" schema:"type:dict,additional_attrs:true" description:"authSession"`
AuthDiscovery AuthDiscovery `yaml:"authDiscovery" schema:"type:dict,additional_attrs:true" description:"authDiscovery"`
TargetPortal string `yaml:"targetPortal" schema:"type:string,required:true" description:"targetPortal"`
IQN string `yaml:"iqn" schema:"type:string,required:true" description:"iqn"`
LUN int `yaml:"lun" schema:"type:int,default:0" description:"lun"`
AuthSession AuthSession `yaml:"authSession" schema:"type:dict,additional_attrs:true" description:"authSession"`
AuthDiscovery AuthDiscovery `yaml:"authDiscovery" schema:"type:dict,additional_attrs:true" description:"authDiscovery"`
}
// AuthSession represents the schema for authentication session in iSCSI Options.
type AuthSession struct {
Username string `yaml:"username" schema:"type:string" description:"username"`
Password string `yaml:"password" schema:"type:string" description:"password"`
UsernameInitiator string `yaml:"usernameInitiator" schema:"type:string" description:"usernameInitiator"`
PasswordInitiator string `yaml:"passwordInitiator" schema:"type:string" description:"passwordInitiator"`
Username string `yaml:"username" schema:"type:string" description:"username"`
Password string `yaml:"password" schema:"type:string" description:"password"`
UsernameInitiator string `yaml:"usernameInitiator" schema:"type:string" description:"usernameInitiator"`
PasswordInitiator string `yaml:"passwordInitiator" schema:"type:string" description:"passwordInitiator"`
}
// AuthDiscovery represents the schema for authentication discovery in iSCSI Options.
type AuthDiscovery struct {
Username string `yaml:"username" schema:"type:string" description:"username"`
Password string `yaml:"password" schema:"type:string" description:"password"`
UsernameInitiator string `yaml:"usernameInitiator" schema:"type:string" description:"usernameInitiator"`
PasswordInitiator string `yaml:"passwordInitiator" schema:"type:string" description:"passwordInitiator"`
Username string `yaml:"username" schema:"type:string" description:"username"`
Password string `yaml:"password" schema:"type:string" description:"password"`
UsernameInitiator string `yaml:"usernameInitiator" schema:"type:string" description:"usernameInitiator"`
PasswordInitiator string `yaml:"passwordInitiator" schema:"type:string" description:"passwordInitiator"`
}
// AutoPermissions represents the schema for Automatic Permissions Configuration.
type AutoPermissions struct {
Enabled bool `yaml:"enabled" schema:"type:boolean,default:false,show_subquestions_if:true" description:"enabled"`
Chown bool `yaml:"chown" schema:"show_if:[[enabled,=,true]],type:boolean,default:false" description:"Run CHOWN"`
Chmod string `yaml:"chmod" schema:"show_if:[[enabled,=,true]],type:string,valid_chars:'[0-9]{3}'" description:"Run CHMOD"`
Recursive bool `yaml:"recursive" schema:"show_if:[[enabled,=,true]],type:boolean,default:false" description:"Recursive"`
Enabled bool `yaml:"enabled" schema:"type:boolean,default:false,show_subquestions_if:true" description:"enabled"`
Chown bool `yaml:"chown" schema:"show_if:[[enabled,=,true]],type:boolean,default:false" description:"Run CHOWN"`
Chmod string `yaml:"chmod" schema:"show_if:[[enabled,=,true]],type:string,valid_chars:'[0-9]{3}'" description:"Run CHMOD"`
Recursive bool `yaml:"recursive" schema:"show_if:[[enabled,=,true]],type:boolean,default:false" description:"Recursive"`
}
// HostPathOptions represents the schema for Host Path Options.
type HostPathOptions struct {
Path string `yaml:"path" schema:"type:string,required:true" description:"Path inside the container the storage is mounted"`
Path string `yaml:"path" schema:"type:string,required:true" description:"Path inside the container the storage is mounted"`
}
// StaticBinding represents the schema for Static Fixed PVC Bindings.
type StaticBinding struct {
Mode string `yaml:"mode" schema:"type:string,default:disabled,enum:disabled,smb,nfs" description:"mode"`
Server string `yaml:"server" schema:"show_if:[[mode,!=,disabled]],type:string,default:'myserver'" description:"Server"`
Share string `yaml:"share" schema:"show_if:[[mode,!=,disabled]],type:string,default:'/myshare'" description:"Share"`
User string `yaml:"user" schema:"show_if:[[mode,=,smb]],type:string,default:'myuser'" description:"User"`
Domain string `yaml:"domain" schema:"show_if:[[mode,=,smb]],type:string" description:"Domain"`
Password string `yaml:"password" schema:"show_if:[[mode,=,smb]],type:string" description:"Password"`
Mode string `yaml:"mode" schema:"type:string,default:disabled,enum:disabled,smb,nfs" description:"mode"`
Server string `yaml:"server" schema:"show_if:[[mode,!=,disabled]],type:string,default:'myserver'" description:"Server"`
Share string `yaml:"share" schema:"show_if:[[mode,!=,disabled]],type:string,default:'/myshare'" description:"Share"`
User string `yaml:"user" schema:"show_if:[[mode,=,smb]],type:string,default:'myuser'" description:"User"`
Domain string `yaml:"domain" schema:"show_if:[[mode,=,smb]],type:string" description:"Domain"`
Password string `yaml:"password" schema:"show_if:[[mode,=,smb]],type:string" description:"Password"`
}
// VolumeSnapshot represents the schema for Volume Snapshots.
type VolumeSnapshot struct {
Name string `yaml:"name" schema:"type:string,default:mysnapshot" description:"Name"`
VolumeSnapshotClassName string `yaml:"volumeSnapshotClassName" schema:"type:string" description:"volumeSnapshot Class Name (Advanced)"`
Name string `yaml:"name" schema:"type:string,default:mysnapshot" description:"Name"`
VolumeSnapshotClassName string `yaml:"volumeSnapshotClassName" schema:"type:string" description:"volumeSnapshot Class Name (Advanced)"`
}
// Persistence represents the schema for Integrated Persistent Storage.
type Persistence struct {
Name string `yaml:"name,omitempty" schema:"type:string" description:"Custom storage name"`
Enabled bool `yaml:"enabled" schema:"type:boolean,default:true,hidden:true" description:"Enable Integrated Persistent Storage"`
Type string `yaml:"type" schema:"type:string,default:pvc,enum:pvc,hostPath,emptyDir,nfs,iscsi" description:"Sets the persistence type, Anything other than PVC could break rollback!"`
Server string `yaml:"server" schema:"show_if:[[type,=,nfs]]type:string default:''" description:"NFS Server"`
Path string `yaml:"path" schema:"show_if:[[type,=,nfs]],type:string" description:"Path on NFS Server"`
ISCSI ISCSIOptions `yaml:"iscsi" schema:"show_if:[[type,=,iscsi]],type:dict,additional_attrs:true" description:"iSCSI Options"`
AutoPermissions AutoPermissions `yaml:"autoPermissions" schema:"show_if:[[type,!=,pvc]],type:dict,additional_attrs:true" description:"Automatic Permissions Configuration"`
ReadOnly bool `yaml:"readOnly" schema:"type:boolean,default:false" description:"Read Only"`
HostPath HostPathOptions `yaml:"hostPath" schema:"show_if:[[type,=,hostPath]],type:hostpath" description:"Host Path"`
MountPath string `yaml:"mountPath" schema:"type:string,required:true,valid_chars:^\\/([a-zA-Z0-9._-]+(\\s?[a-zA-Z0.9._-]+|\\/?)$" description:"Path inside the container the storage is mounted"`
Medium string `yaml:"medium" schema:"show_if:[[type,=,emptyDir]],type:string,enum:'Memory'" description:"EmptyDir Medium"`
Size string `yaml:"size" schema:"show_if:[[type,=,pvc]],type:string,default:256Gi" description:"Size Quotum of Storage"`
StorageClass string `yaml:"storageClass" schema:"show_if:[[type,=,pvc]],type:string" description:"storageClass (Advanced)"`
Static StaticBinding `yaml:"static" schema:"show_if:[[type,=,pvc]],type:dict,additional_attrs:true" description:"Static Fixed PVC Bindings (Experimental)"`
VolumeSnapshots []VolumeSnapshot `yaml:"volumeSnapshots" schema:"show_if:[[type,=,pvc]],type:list,default:[]" description:"Volume Snapshots (Experimental)"`
Name string `yaml:"name,omitempty" schema:"type:string" description:"Custom storage name"`
Enabled bool `yaml:"enabled" schema:"type:boolean,default:true,hidden:true" description:"Enable Integrated Persistent Storage"`
Type string `yaml:"type" schema:"type:string,default:pvc,enum:pvc,hostPath,emptyDir,nfs,iscsi" description:"Sets the persistence type, Anything other than PVC could break rollback!"`
Server string `yaml:"server" schema:"show_if:[[type,=,nfs]]type:string default:''" description:"NFS Server"`
Path string `yaml:"path" schema:"show_if:[[type,=,nfs]],type:string" description:"Path on NFS Server"`
ISCSI ISCSIOptions `yaml:"iscsi" schema:"show_if:[[type,=,iscsi]],type:dict,additional_attrs:true" description:"iSCSI Options"`
AutoPermissions AutoPermissions `yaml:"autoPermissions" schema:"show_if:[[type,!=,pvc]],type:dict,additional_attrs:true" description:"Automatic Permissions Configuration"`
ReadOnly bool `yaml:"readOnly" schema:"type:boolean,default:false" description:"Read Only"`
HostPath HostPathOptions `yaml:"hostPath" schema:"show_if:[[type,=,hostPath]],type:hostpath" description:"Host Path"`
MountPath string `yaml:"mountPath" schema:"type:string,required:true,valid_chars:^\\/([a-zA-Z0-9._-]+(\\s?[a-zA-Z0.9._-]+|\\/?)$" description:"Path inside the container the storage is mounted"`
Medium string `yaml:"medium" schema:"show_if:[[type,=,emptyDir]],type:string,enum:'Memory'" description:"EmptyDir Medium"`
Size string `yaml:"size" schema:"show_if:[[type,=,pvc]],type:string,default:256Gi" description:"Size Quotum of Storage"`
StorageClass string `yaml:"storageClass" schema:"show_if:[[type,=,pvc]],type:string" description:"storageClass (Advanced)"`
Static StaticBinding `yaml:"static" schema:"show_if:[[type,=,pvc]],type:dict,additional_attrs:true" description:"Static Fixed PVC Bindings (Experimental)"`
VolumeSnapshots []VolumeSnapshot `yaml:"volumeSnapshots" schema:"show_if:[[type,=,pvc]],type:list,default:[]" description:"Volume Snapshots (Experimental)"`
}
@@ -2,22 +2,22 @@ package valuesYaml
// DNSConfigEntry represents an entry in the DNS configuration.
type DNSConfigEntry struct {
Name string `yaml:"name,omitempty" validate:"required" description:"Name"`
Value string `yaml:"value,omitempty" validate:"required" description:"Value"`
Name string `yaml:"name,omitempty" validate:"required" description:"Name"`
Value string `yaml:"value,omitempty" validate:"required" description:"Value"`
}
// PodOptions represents the "Global Pod Options (Advanced)" section of the schema.
type PodOptions struct {
ExpertPodOpts struct {
Type bool `yaml:"expertPodOpts" description:"Expert - Pod Options" default:"false" show_subquestions_if:"true"`
HostNetwork bool `yaml:"hostNetwork,omitempty" description:"Host Networking" default:"false"`
DNSConfig DNSConfig `yaml:"dnsConfig,omitempty" description:"DNS Configuration"`
} `yaml:"podOptions,omitempty" description:"Global Pod Options (Advanced)"`
ExpertPodOpts struct {
Type bool `yaml:"expertPodOpts" description:"Expert - Pod Options" default:"false" show_subquestions_if:"true"`
HostNetwork bool `yaml:"hostNetwork,omitempty" description:"Host Networking" default:"false"`
DNSConfig DNSConfig `yaml:"dnsConfig,omitempty" description:"DNS Configuration"`
} `yaml:"podOptions,omitempty" description:"Global Pod Options (Advanced)"`
}
// DNSConfig represents the DNS configuration.
type DNSConfig struct {
Options []DNSConfigEntry `yaml:"options,omitempty" validate:"dive" description:"Options"`
Nameservers []string `yaml:"nameservers,omitempty" validate:"dive,required" description:"Nameservers"`
Searches []string `yaml:"searches,omitempty" validate:"dive,required" description:"Searches"`
Options []DNSConfigEntry `yaml:"options,omitempty" validate:"dive" description:"Options"`
Nameservers []string `yaml:"nameservers,omitempty" validate:"dive,required" description:"Nameservers"`
Searches []string `yaml:"searches,omitempty" validate:"dive,required" description:"Searches"`
}
@@ -2,42 +2,42 @@ package valuesYaml
// Resources represents the schema for resource settings.
type Resources struct {
Limits ResourceLimits `yaml:"limits" schema:"additional_attrs:true,type:dict"`
Requests ResourceLimits `yaml:"requests" schema:"additional_attrs:true,type:dict,hidden:true"`
Limits ResourceLimits `yaml:"limits" schema:"additional_attrs:true,type:dict"`
Requests ResourceLimits `yaml:"requests" schema:"additional_attrs:true,type:dict,hidden:true"`
}
// ResourceLimits represents the schema for resource limit settings.
type ResourceLimits struct {
CPU string `yaml:"cpu" schema:"type:string,default:4000m,valid_chars:^(?!^0(\\.0|m|)$)([0-9]+)(\\.[0-9]|m?)$"`
Memory string `yaml:"memory" schema:"type:string,default:8Gi,valid_chars:^(?!^0(e[0-9]|[EPTGMK]i?|)$)([0-9]+)(|[EPTGMK]i?|e[0-9]+)$"`
CPU string `yaml:"cpu" schema:"type:string,default:4000m,valid_chars:^(?!^0(\\.0|m|)$)([0-9]+)(\\.[0-9]|m?)$"`
Memory string `yaml:"memory" schema:"type:string,default:8Gi,valid_chars:^(?!^0(e[0-9]|[EPTGMK]i?|)$)([0-9]+)(|[EPTGMK]i?|e[0-9]+)$"`
}
// DeviceList represents the schema for the list of devices.
type DeviceList struct {
DeviceListEntry []DeviceEntry `yaml:"deviceList" schema:"type:list,default:[]"`
DeviceListEntry []DeviceEntry `yaml:"deviceList" schema:"type:list,default:[]"`
}
// DeviceEntry represents the schema for a device entry.
type DeviceEntry struct {
Enabled bool `yaml:"enabled" schema:"type:boolean,default:true"`
Type string `yaml:"type" schema:"type:string,default:device,hidden:true"`
ReadOnly bool `yaml:"readOnly" schema:"type:boolean,default:false"`
HostPath string `yaml:"hostPath" schema:"type:path"`
MountPath string `yaml:"mountPath" schema:"type:string,default:/dev/ttyACM0"`
Enabled bool `yaml:"enabled" schema:"type:boolean,default:true"`
Type string `yaml:"type" schema:"type:string,default:device,hidden:true"`
ReadOnly bool `yaml:"readOnly" schema:"type:boolean,default:false"`
HostPath string `yaml:"hostPath" schema:"type:path"`
MountPath string `yaml:"mountPath" schema:"type:string,default:/dev/ttyACM0"`
}
// ScaleGPU represents the schema for GPU configuration.
type ScaleGPU struct {
ScaleGPUEntry []GPUEntry `yaml:"scaleGPU" schema:"type:list,default:[]"`
ScaleGPUEntry []GPUEntry `yaml:"scaleGPU" schema:"type:list,default:[]"`
}
// GPUEntry represents the schema for a GPU entry.
type GPUEntry struct {
GPU GPUConfiguration `yaml:"gpu" schema:"additional_attrs:true,type:dict"`
Workaround string `yaml:"workaround" schema:"type:string,default:workaround,hidden:true"`
GPU GPUConfiguration `yaml:"gpu" schema:"additional_attrs:true,type:dict"`
Workaround string `yaml:"workaround" schema:"type:string,default:workaround,hidden:true"`
}
// GPUConfiguration represents the schema for GPU configuration.
type GPUConfiguration struct {
// Specify GPU configuration here
// Specify GPU configuration here
}
@@ -2,24 +2,24 @@ package valuesYaml
// Container represents the schema for container settings.
type Container struct {
RunAsUser int `yaml:"runAsUser" schema:"type:int,default:568"`
RunAsGroup int `yaml:"runAsGroup" schema:"type:int,default:568"`
PUID int `yaml:"PUID" schema:"type:int,default:568,show_if:[[runAsUser,=,0]]"`
UMASK string `yaml:"UMASK" schema:"type:string,default:0022"`
Advanced bool `yaml:"advanced" schema:"type:boolean,default:false,show_subquestions_if:true"`
Privileged bool `yaml:"privileged" schema:"type:boolean,default:false,show_if:[[advanced,=,true]]"`
ReadOnlyRootFilesystem bool `yaml:"readOnlyRootFilesystem" schema:"type:boolean,default:true,show_if:[[advanced,=,true]]"`
RunAsUser int `yaml:"runAsUser" schema:"type:int,default:568"`
RunAsGroup int `yaml:"runAsGroup" schema:"type:int,default:568"`
PUID int `yaml:"PUID" schema:"type:int,default:568,show_if:[[runAsUser,=,0]]"`
UMASK string `yaml:"UMASK" schema:"type:string,default:0022"`
Advanced bool `yaml:"advanced" schema:"type:boolean,default:false,show_subquestions_if:true"`
Privileged bool `yaml:"privileged" schema:"type:boolean,default:false,show_if:[[advanced,=,true]]"`
ReadOnlyRootFilesystem bool `yaml:"readOnlyRootFilesystem" schema:"type:boolean,default:true,show_if:[[advanced,=,true]]"`
}
// Pod represents the schema for pod settings.
type Pod struct {
FsGroupChangePolicy string `yaml:"fsGroupChangePolicy" schema:"type:string,default:OnRootMismatch,enum:OnRootMismatch,Always"`
SupplementalGroups []int `yaml:"supplementalGroups" schema:"type:list,default:[],items:type:int"`
FsGroup int `yaml:"fsGroup" schema:"type:int,default:568"`
FsGroupChangePolicy string `yaml:"fsGroupChangePolicy" schema:"type:string,default:OnRootMismatch,enum:OnRootMismatch,Always"`
SupplementalGroups []int `yaml:"supplementalGroups" schema:"type:list,default:[],items:type:int"`
FsGroup int `yaml:"fsGroup" schema:"type:int,default:568"`
}
// SecurityContext represents the schema for security context settings.
type SecurityContext struct {
Container Container `yaml:"container" schema:"additional_attrs:true,type:dict"`
Pod Pod `yaml:"pod" schema:"additional_attrs:true,type:dict"`
Container Container `yaml:"container" schema:"additional_attrs:true,type:dict"`
Pod Pod `yaml:"pod" schema:"additional_attrs:true,type:dict"`
}
@@ -2,27 +2,27 @@ package valuesYaml
// PortConfiguration represents the configuration for a service port.
type PortConfiguration struct {
Enabled bool `yaml:"enabled" schema:"type:boolean" default:"true" hidden:"true" description:"Enable the Port"`
Name string `yaml:"name" schema:"type:string" default:"" description:"Port Name"`
Protocol string `yaml:"protocol" schema:"type:string" default:"tcp" enum:"[http, https, tcp, udp]" description:"Port Type"`
TargetPort int `yaml:"targetPort" schema:"type:int" required:"true" description:"Target Port"`
Port int `yaml:"port" schema:"type:int" required:"true" description:"Container Port"`
Enabled bool `yaml:"enabled" schema:"type:boolean" default:"true" hidden:"true" description:"Enable the Port"`
Name string `yaml:"name" schema:"type:string" default:"" description:"Port Name"`
Protocol string `yaml:"protocol" schema:"type:string" default:"tcp" enum:"[http, https, tcp, udp]" description:"Port Type"`
TargetPort int `yaml:"targetPort" schema:"type:int" required:"true" description:"Target Port"`
Port int `yaml:"port" schema:"type:int" required:"true" description:"Container Port"`
}
// AdvancedServiceSettings represents the advanced settings for a service.
type AdvancedServiceSettings struct {
ExternalIPs []string `yaml:"externalIPs" schema:"type:list" default:"[]" items:"type:string" description:"External IP's"`
IPFamilyPolicy string `yaml:"ipFamilyPolicy" schema:"type:string" default:"SingleStack" enum:"[SingleStack, PreferDualStack, RequireDualStack]" description:"IP Family Policy"`
IPFamilies []string `yaml:"ipFamilies" schema:"type:list" default:"[]" items:"type:string" description:"(Advanced) The IP Families that should be used"`
ExternalIPs []string `yaml:"externalIPs" schema:"type:list" default:"[]" items:"type:string" description:"External IP's"`
IPFamilyPolicy string `yaml:"ipFamilyPolicy" schema:"type:string" default:"SingleStack" enum:"[SingleStack, PreferDualStack, RequireDualStack]" description:"IP Family Policy"`
IPFamilies []string `yaml:"ipFamilies" schema:"type:list" default:"[]" items:"type:string" description:"(Advanced) The IP Families that should be used"`
}
// ServiceConfiguration represents the configuration for a service.
type ServiceConfiguration struct {
Enabled bool `yaml:"enabled" schema:"type:boolean" default:"true" hidden:"true" description:"Enable the service"`
Name string `yaml:"name" schema:"type:string" default:"" description:"Name"`
Type string `yaml:"type" schema:"type:string" default:"LoadBalancer" enum:"[LoadBalancer, ClusterIP, Simple]" description:"Service Type"`
LoadBalancerIP string `yaml:"loadBalancerIP" schema:"type:string" show_if:"[['type', '=', 'LoadBalancer']]" default:"" description:"LoadBalancer IP"`
AdvancedSvcSet AdvancedServiceSettings `yaml:"advancedsvcset" schema:"type:boolean" default:"false" show_subquestions_if:"true" description:"Show Advanced Service Settings"`
Ports PortConfiguration `yaml:"ports" schema:"type:dict" description:"Service's Port(s) Configuration"`
PortsList []PortConfiguration `yaml:"portsList" schema:"type:list" default:"[]" items:"type:dict" description:"Additional Service Ports"`
Enabled bool `yaml:"enabled" schema:"type:boolean" default:"true" hidden:"true" description:"Enable the service"`
Name string `yaml:"name" schema:"type:string" default:"" description:"Name"`
Type string `yaml:"type" schema:"type:string" default:"LoadBalancer" enum:"[LoadBalancer, ClusterIP, Simple]" description:"Service Type"`
LoadBalancerIP string `yaml:"loadBalancerIP" schema:"type:string" show_if:"[['type', '=', 'LoadBalancer']]" default:"" description:"LoadBalancer IP"`
AdvancedSvcSet AdvancedServiceSettings `yaml:"advancedsvcset" schema:"type:boolean" default:"false" show_subquestions_if:"true" description:"Show Advanced Service Settings"`
Ports PortConfiguration `yaml:"ports" schema:"type:dict" description:"Service's Port(s) Configuration"`
PortsList []PortConfiguration `yaml:"portsList" schema:"type:list" default:"[]" items:"type:dict" description:"Additional Service Ports"`
}
+28 -28
View File
@@ -1,44 +1,44 @@
package valuesYaml
import (
"fmt"
"os"
"path/filepath"
"fmt"
"os"
"path/filepath"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog/log"
)
// UpdatevaluesFile updates the specified values.yaml file with an optional bump parameter.
func UpdatevaluesFile(valuesPathOrFolder, bump string) error {
var chartFolder string
var valuesPath string
var chartFolder string
var valuesPath string
fileInfo, err := os.Stat(valuesPathOrFolder)
if err != nil {
return err
}
fileInfo, err := os.Stat(valuesPathOrFolder)
if err != nil {
return err
}
if fileInfo.IsDir() {
chartFolder = valuesPathOrFolder
} else {
chartFolder = filepath.Dir(valuesPathOrFolder)
if fileInfo.IsDir() {
chartFolder = valuesPathOrFolder
} else {
chartFolder = filepath.Dir(valuesPathOrFolder)
}
valuesPath = filepath.Join(chartFolder, "values.yaml")
}
valuesPath = filepath.Join(chartFolder, "values.yaml")
log.Printf("Processing: %s\n", valuesPath)
values := NewValuesFile()
if err := values.LoadFromFile(valuesPath); err != nil {
log.Info().Msgf("Error loading values: %v\n", err)
return err
}
log.Printf("Processing: %s\n", valuesPath)
values := NewValuesFile()
if err := values.LoadFromFile(valuesPath); err != nil {
log.Info().Msgf("Error loading values: %v\n", err)
return err
}
// Save the modified metadata back to the file
if err := values.SaveToFile(valuesPath); err != nil {
return fmt.Errorf("error saving values.yaml: %s", err)
}
// Save the modified metadata back to the file
if err := values.SaveToFile(valuesPath); err != nil {
return fmt.Errorf("error saving values.yaml: %s", err)
}
log.Printf("values file updated and saved to %s\n", valuesPath)
log.Printf("values file updated and saved to %s\n", valuesPath)
return nil
return nil
}
@@ -2,52 +2,52 @@ package valuesYaml
// WorkloadRootReference represents the schema for the root reference in workload settings.
type WorkloadRootReference struct {
Type string `yaml:"type,omitempty" schema:"type:string,default:Deployment,enum:,Deployment,DaemonSet"`
Replicas int `yaml:"replicas,omitempty" schema:"type:int,show_if:[[type,!=,DaemonSet]],default:1"`
PodSpec WorkloadPodSpec `yaml:"podSpec,omitempty" schema:"additional_attrs:true,type:dict"`
UpdateStrategy string `yaml:"updateStrategy,omitempty"`
Type string `yaml:"type,omitempty" schema:"type:string,default:Deployment,enum:,Deployment,DaemonSet"`
Replicas int `yaml:"replicas,omitempty" schema:"type:int,show_if:[[type,!=,DaemonSet]],default:1"`
PodSpec WorkloadPodSpec `yaml:"podSpec,omitempty" schema:"additional_attrs:true,type:dict"`
UpdateStrategy string `yaml:"updateStrategy,omitempty"`
}
// WorkloadPodSpec represents the schema for the pod spec in workload settings.
type WorkloadPodSpec struct {
Containers WorkloadContainers `yaml:"containers,omitempty" schema:"additional_attrs:true,type:dict"`
Containers WorkloadContainers `yaml:"containers,omitempty" schema:"additional_attrs:true,type:dict"`
}
// WorkloadContainers represents the schema for containers in workload settings.
type WorkloadContainers struct {
ContainerItem WorkloadContainerItem `yaml:"containerItem,omitempty" schema:"additional_attrs:true,type:dict"`
ContainerItem WorkloadContainerItem `yaml:"containerItem,omitempty" schema:"additional_attrs:true,type:dict"`
}
// WorkloadContainerItem represents the schema for a container item in workload settings.
type WorkloadContainerItem struct {
Env map[string]string `yaml:"env,omitempty" schema:"additional_attrs:true,type:dict"`
EnvList []EnvList `yaml:"envList,omitempty" schema:"type:list,default:[],items:type:dict"`
ExtraArgs []string `yaml:"extraArgs,omitempty" schema:"type:list,default:[]"`
Advanced WorkloadContainerAdvanced `yaml:"advanced,omitempty" schema:"type:boolean,default:false,show_subquestions_if:true"`
Probes WorkloadProbes `yaml:"probes,omitempty"`
UpdateStrategy string `yaml:"updateStrategy,omitempty"`
Env map[string]string `yaml:"env,omitempty" schema:"additional_attrs:true,type:dict"`
EnvList []EnvList `yaml:"envList,omitempty" schema:"type:list,default:[],items:type:dict"`
ExtraArgs []string `yaml:"extraArgs,omitempty" schema:"type:list,default:[]"`
Advanced WorkloadContainerAdvanced `yaml:"advanced,omitempty" schema:"type:boolean,default:false,show_subquestions_if:true"`
Probes WorkloadProbes `yaml:"probes,omitempty"`
UpdateStrategy string `yaml:"updateStrategy,omitempty"`
}
// EnvList represents the schema for an environment variable list item in workload settings.
type EnvList struct {
Name string `yaml:"name,omitempty" schema:"type:string"`
Value string `yaml:"value,omitempty" schema:"type:string"`
Name string `yaml:"name,omitempty" schema:"type:string"`
Value string `yaml:"value,omitempty" schema:"type:string"`
}
// WorkloadContainerAdvanced represents the schema for advanced settings in the workload container settings.
type WorkloadContainerAdvanced struct {
Command []string `yaml:"command,omitempty" schema:"type:list,default:[],items:type:string"`
ExtraSettings map[string]string `yaml:"extraSettings,omitempty" schema:"type:dict"`
Command []string `yaml:"command,omitempty" schema:"type:list,default:[],items:type:string"`
ExtraSettings map[string]string `yaml:"extraSettings,omitempty" schema:"type:dict"`
}
// WorkloadProbes represents the schema for probes in workload container settings.
type WorkloadProbes struct {
Liveness Probe `yaml:"liveness,omitempty"`
Readiness Probe `yaml:"readiness,omitempty"`
Startup Probe `yaml:"startup,omitempty"`
Liveness Probe `yaml:"liveness,omitempty"`
Readiness Probe `yaml:"readiness,omitempty"`
Startup Probe `yaml:"startup,omitempty"`
}
// Probe represents the schema for a probe in workload container settings.
type Probe struct {
Path string `yaml:"path,omitempty"`
Path string `yaml:"path,omitempty"`
}
+33 -33
View File
@@ -1,51 +1,51 @@
package version
import (
"fmt"
"regexp"
"fmt"
"regexp"
"github.com/Masterminds/semver/v3"
"github.com/rs/zerolog/log"
"github.com/Masterminds/semver/v3"
"github.com/rs/zerolog/log"
)
const (
Major = "major"
Minor = "minor"
Patch = "patch"
Major = "major"
Minor = "minor"
Patch = "patch"
)
func IncrementVersion(version, kind string) (string, error) {
// Validate SemVer format
semVerPattern := regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)$`)
if !semVerPattern.MatchString(version) {
return "", fmt.Errorf("invalid SemVer format (Major.Minor.Patch): %s", version)
}
// Validate SemVer format
semVerPattern := regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)$`)
if !semVerPattern.MatchString(version) {
return "", fmt.Errorf("invalid SemVer format (Major.Minor.Patch): %s", version)
}
v, err := semver.NewVersion(version)
if err != nil {
return "", err
}
v, err := semver.NewVersion(version)
if err != nil {
return "", err
}
// Increment the specified version component
switch kind {
case Major:
return v.IncMajor().String(), nil
case Minor:
return v.IncMinor().String(), nil
case Patch:
return v.IncPatch().String(), nil
default:
return "", fmt.Errorf("invalid bump kind: %s", kind)
}
// Increment the specified version component
switch kind {
case Major:
return v.IncMajor().String(), nil
case Minor:
return v.IncMinor().String(), nil
case Patch:
return v.IncPatch().String(), nil
default:
return "", fmt.Errorf("invalid bump kind: %s", kind)
}
}
func Bump(semVer, kind string) error {
newVersion, err := IncrementVersion(semVer, kind)
if err != nil {
log.Fatal().Err(err).Msg("Failed to increment version")
}
newVersion, err := IncrementVersion(semVer, kind)
if err != nil {
log.Fatal().Err(err).Msg("Failed to increment version")
}
log.Info().Msgf("🆚 Updated SemVer from [%s] to [%s]", semVer, newVersion)
return nil
log.Info().Msgf("🆚 Updated SemVer from [%s] to [%s]", semVer, newVersion)
return nil
}
+69 -69
View File
@@ -3,76 +3,76 @@ package version
import "testing"
func TestIncrementVersion(t *testing.T) {
type args struct {
version string
kind string
}
type args struct {
version string
kind string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "Test increment major",
args: args{
version: "1.2.3",
kind: Major,
},
want: "2.0.0",
wantErr: false,
},
{
name: "Test increment minor",
args: args{
version: "1.2.3",
kind: Minor,
},
want: "1.3.0",
wantErr: false,
},
{
name: "Test increment patch",
args: args{
version: "1.2.3",
kind: Patch,
},
want: "1.2.4",
wantErr: false,
},
{
name: "Test increment invalid",
args: args{
version: "1.2.3",
kind: "invalid",
},
want: "",
wantErr: true,
},
{
name: "Test increment incomplete",
args: args{
version: "1.2",
kind: "invalid",
},
want: "",
wantErr: true,
},
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "Test increment major",
args: args{
version: "1.2.3",
kind: Major,
},
want: "2.0.0",
wantErr: false,
},
{
name: "Test increment minor",
args: args{
version: "1.2.3",
kind: Minor,
},
want: "1.3.0",
wantErr: false,
},
{
name: "Test increment patch",
args: args{
version: "1.2.3",
kind: Patch,
},
want: "1.2.4",
wantErr: false,
},
{
name: "Test increment invalid",
args: args{
version: "1.2.3",
kind: "invalid",
},
want: "",
wantErr: true,
},
{
name: "Test increment incomplete",
args: args{
version: "1.2",
kind: "invalid",
},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := IncrementVersion(tt.args.version, tt.args.kind)
// If we expected an error, but didn't get one, fail the test
if (err != nil) != tt.wantErr {
t.Errorf("IncrementVersion() error = %v, wantErr %t", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("IncrementVersion() got = %v, want %v", got, tt.want)
}
})
}
got, err := IncrementVersion(tt.args.version, tt.args.kind)
// If we expected an error, but didn't get one, fail the test
if (err != nil) != tt.wantErr {
t.Errorf("IncrementVersion() error = %v, wantErr %t", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("IncrementVersion() got = %v, want %v", got, tt.want)
}
})
}
}
+86 -86
View File
@@ -1,119 +1,119 @@
package website
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"slices"
"sync"
"encoding/json"
"fmt"
"os"
"path/filepath"
"slices"
"sync"
"github.com/truecharts/public/clustertool/pkg/charts/chartFile"
"github.com/truecharts/public/clustertool/pkg/helper"
"github.com/truecharts/public/clustertool/pkg/charts/chartFile"
"github.com/truecharts/public/clustertool/pkg/helper"
)
type ChartList struct {
TotalCount int64 `json:"totalCount"`
Trains []Train `json:"trains"`
TotalCount int64 `json:"totalCount"`
Trains []Train `json:"trains"`
}
type Train struct {
Name string `json:"name"`
Count int64 `json:"count"`
Charts []Chart `json:"charts"`
Name string `json:"name"`
Count int64 `json:"count"`
Charts []Chart `json:"charts"`
}
type Chart struct {
Name string `json:"name"`
Description string `json:"description"`
Train string `json:"train"`
Link string `json:"link"`
Icon string `json:"icon"`
Version string `json:"version"`
Name string `json:"name"`
Description string `json:"description"`
Train string `json:"train"`
Link string `json:"link"`
Icon string `json:"icon"`
Version string `json:"version"`
}
type ChartListOptions struct {
OutputPath string // Path to put the chart list json file
TrainFilter []string // Empty means all trains
sync.Mutex
list *ChartList
OutputPath string // Path to put the chart list json file
TrainFilter []string // Empty means all trains
sync.Mutex
list *ChartList
}
func (o *ChartListOptions) WriteChartList() error {
if o.list == nil {
return fmt.Errorf("chart list is nil")
}
if o.list == nil {
return fmt.Errorf("chart list is nil")
}
data, err := json.Marshal(o.list)
if err != nil {
return err
}
data, err := json.Marshal(o.list)
if err != nil {
return err
}
return os.WriteFile(o.OutputPath, data, 0644)
return os.WriteFile(o.OutputPath, data, 0644)
}
func (o *ChartListOptions) GetChartData(path string, entry os.DirEntry, err error) error {
if o.list == nil {
o.list = &ChartList{}
}
if o.list == nil {
o.list = &ChartList{}
}
if err != nil {
return err
}
if err != nil {
return err
}
// Skip directories that are excluded
if entry.IsDir() && slices.Contains(helper.ExcludedDirs, entry.Name()) {
return filepath.SkipDir
}
// Skip directories that are excluded
if entry.IsDir() && slices.Contains(helper.ExcludedDirs, entry.Name()) {
return filepath.SkipDir
}
if entry.Name() != "Chart.yaml" {
return nil
}
if entry.Name() != "Chart.yaml" {
return nil
}
chart := chartFile.NewHelmChart()
if err := chart.LoadFromFile(path); err != nil {
return err
}
chart := chartFile.NewHelmChart()
if err := chart.LoadFromFile(path); err != nil {
return err
}
train := chartFile.GetTrain(path, chart)
if len(o.TrainFilter) > 0 {
if !slices.Contains(o.TrainFilter, train) {
return nil
}
}
train := chartFile.GetTrain(path, chart)
if len(o.TrainFilter) > 0 {
if !slices.Contains(o.TrainFilter, train) {
return nil
}
}
o.Lock()
defer o.Unlock()
// Increment the total count
o.list.TotalCount++
webChart := Chart{
Name: chart.Metadata.Name,
Description: chart.Metadata.Description,
Icon: chart.Metadata.Icon,
Link: chart.Metadata.Home,
Version: chart.Metadata.Version,
Train: chartFile.GetTrain(path, chart),
}
o.Lock()
defer o.Unlock()
// Increment the total count
o.list.TotalCount++
webChart := Chart{
Name: chart.Metadata.Name,
Description: chart.Metadata.Description,
Icon: chart.Metadata.Icon,
Link: chart.Metadata.Home,
Version: chart.Metadata.Version,
Train: chartFile.GetTrain(path, chart),
}
trainExists := false
for idx, train := range o.list.Trains {
if train.Name == webChart.Train {
trainExists = true
// Increase chart count for the existing train
o.list.Trains[idx].Count++
// Add the chart to the existing train
o.list.Trains[idx].Charts = append(o.list.Trains[idx].Charts, webChart)
}
}
if trainExists {
return nil
}
trainExists := false
for idx, train := range o.list.Trains {
if train.Name == webChart.Train {
trainExists = true
// Increase chart count for the existing train
o.list.Trains[idx].Count++
// Add the chart to the existing train
o.list.Trains[idx].Charts = append(o.list.Trains[idx].Charts, webChart)
}
}
if trainExists {
return nil
}
// Add a new train with the chart
o.list.Trains = append(o.list.Trains, Train{
Name: webChart.Train,
Count: 1,
Charts: []Chart{webChart},
})
// Add a new train with the chart
o.list.Trains = append(o.list.Trains, Train{
Name: webChart.Train,
Count: 1,
Charts: []Chart{webChart},
})
return nil
return nil
}