Skip to content

Commit

Permalink
Adding OpenCost and Workflow for dependency updating
Browse files Browse the repository at this point in the history
  • Loading branch information
davidcollom committed Aug 9, 2024
1 parent d24b294 commit e0ebf9b
Show file tree
Hide file tree
Showing 7 changed files with 943 additions and 17 deletions.
13 changes: 13 additions & 0 deletions .github/workflows/build-helm-chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ concurrency:
cancel-in-progress: true

jobs:
docs:
name: Generate Helm Docs
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Render helm docs inside the README.md
uses: shaybentk/[email protected]
with:
working-dir: chart/
fail-on-diff: true

lint:
permissions:
contents: read # for actions/checkout to fetch code
Expand Down
45 changes: 45 additions & 0 deletions .github/workflows/update-chart-dependencies.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Check for Chart Dependency Updates

on:
workflow_dispatch: # Enable manual generation
schedule:
- cron: '0 0 * * 0'



concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-chart
cancel-in-progress: true

jobs:
check-for-updates:
permissions:
contents: write
pull-requests: write
name: Update Dependencies
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: 'scripts/go.mod'

- name: Run Script
working-directory: scripts/
run: go run chart-dep-updater.go ../chart/Chart.yaml

- name: Creating PR
uses: peter-evans/[email protected]
with:
token: ${{ secrets.JETSTACK_CHARTS_PAT }}
title: "Update finops-stack Chart Dependencies"
commit-message: "Update finops-stack Chart Dependencies"
branch: finops-stack/${{ github.job_workflow_sha }}
path: chart
add-paths: chart/
delete-branch: true
signoff: true
base: main
draft: false
36 changes: 19 additions & 17 deletions chart/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,31 +27,33 @@ dependencies:
- name: vpatron
version: "~0.0.0"
condition: feature.vpatron
repository: https://charts.jetstack.io
# repository: file://../../vpatron/chart/
# repository: https://charts.jetstack.io
repository: file://../../finops-toolkit/vpatron/chart/
- name: autospot
version: "~0.0.0"
condition: feature.autospot
repository: https://charts.jetstack.io
# repository: file://../../autospot/chart/
# repository: https://charts.jetstack.io
repository: file://../../finops-toolkit/autospot/chart/
- name: limit-ranger
version: "~0.0.0"
condition: feature.limit-ranger
repository: https://charts.jetstack.io
# repository: file://../../limit-ranger/chart/
# repository: https://charts.jetstack.io
repository: file://../../finops-toolkit/limit-ranger/chart/
- name: office-hours
version: "~0.0.0"
version: "~> 0.0.0"
condition: feature.office-hours
repository: https://charts.jetstack.io
# repository: file://../../office-hours/chart/
# repository: https://charts.jetstack.io
repository: file://../../finops-toolkit/office-hours/chart/

# OpenCost
# - name: prometheus-opencost-exporter
# version: "*"
# repository: https://prometheus-community.github.io/helm-charts
# - name: opencost
# version: "*"
# repository: https://opencost.github.io/opencost-helm-chart
- name: prometheus-opencost-exporter
version: "~> 0.1.1"
condition: feature.opencost-exporter
repository: https://prometheus-community.github.io/helm-charts
- name: opencost
version: "~> 1.41.0"
condition: feature.opencost
repository: https://opencost.github.io/opencost-helm-chart

# Kyverno
- name: kyverno
Expand All @@ -64,11 +66,11 @@ dependencies:
repository: https://charts.jetstack.io

- name: vpa
version: "*"
version: "4.5.0"
condition: feature.vpa
repository: https://charts.fairwinds.com/stable

- name: grafana
version: "*"
version: "8.4.0"
condition: feature.grafana
repository: https://grafana.github.io/helm-charts
4 changes: 4 additions & 0 deletions chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ feature:
vpa: false
# -- Enable / Disable the installation of the Grafana
grafana: true
# -- Enable / Disable the installation of OpenCost
opencost: true
# -- Enable / Disable the installation of OpenCost Exporter
opencost-exporter: true

# @ignored
vpa:
Expand Down
252 changes: 252 additions & 0 deletions scripts/chart-dep-updater.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
package main

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"

"github.com/Masterminds/semver/v3"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/downloader"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/lint/rules"
"helm.sh/helm/v3/pkg/lint/support"
"helm.sh/helm/v3/pkg/repo"
)

var (
logFormat string
logLevel string
logger = logrus.New()
)

func main() {
var rootCmd = &cobra.Command{
Use: "chart-dep-updater [Chart.yaml]",
Short: "A tool to check for updates in Helm chart dependencies",
Args: cobra.ExactArgs(1),
Run: run,
}

rootCmd.Flags().StringVarP(&logFormat, "log-format", "f", "text", "Log format: text or json")
rootCmd.Flags().StringVarP(&logLevel, "log-level", "l", "info", "Log level: debug, info, warn, error")

if err := rootCmd.Execute(); err != nil {
logger.Fatalf("Error executing command: %v", err)
}
}

func run(cmd *cobra.Command, args []string) {
configureLogger()

chartPath := args[0]

// Load the Chart.yaml using yaml.Node to preserve structure
content, err := ioutil.ReadFile(chartPath)
if err != nil {
logger.Fatalf("Error reading Chart.yaml: %v", err)
}

var rootNode yaml.Node
if err := yaml.Unmarshal(content, &rootNode); err != nil {
logger.Fatalf("Error parsing Chart.yaml: %v", err)
}

updated := false

// Find dependencies node and update versions
for i, node := range rootNode.Content[0].Content {
if node.Value == "dependencies" {
dependencies := rootNode.Content[0].Content[i+1]
for j := 0; j < len(dependencies.Content); j++ {
dep := dependencies.Content[j]
updateNode := false
var name, version, repo string
for k := 0; k < len(dep.Content); k += 2 {
key := dep.Content[k].Value
value := dep.Content[k+1].Value
if key == "name" {
name = value
}
if key == "version" {
version = value
}
if key == "repository" {
repo = value
}
}

latestVersion, err := getLatestVersion(repo, name)
if err != nil {
logger.Errorf("Error fetching latest version for %s: %v", name, err)
continue
}

constraint, err := semver.NewConstraint(version)
if err != nil {
logger.Errorf("Invalid version constraint for %s: %v", name, err)
continue
}

latestVer, err := semver.NewVersion(latestVersion)
if err != nil {
logger.Errorf("Invalid latest version for %s: %v", name, err)
continue
}

if constraint.Check(latestVer) {
logger.Infof("Dependency %s is up-to-date: %s", name, latestVersion)
} else {
logger.Warnf("Dependency %s has a newer version available: %s", name, latestVersion)
// Update the version in the YAML node
for k := 0; k < len(dep.Content); k += 2 {
if dep.Content[k].Value == "version" {
dep.Content[k+1].Value = latestVersion
updateNode = true
break
}
}
updated = updated || updateNode
}
}
break
}
}

if updated {
// Write the updated content back to Chart.yaml
out, err := yaml.Marshal(&rootNode)
if err != nil {
logger.Fatalf("Error marshaling updated Chart.yaml: %v", err)
}

if err := os.WriteFile(chartPath, out, 0644); err != nil {
logger.Fatalf("Error writing updated Chart.yaml: %v", err)
}
logger.Infof("Chart.yaml updated successfully.")

// Run Helm dependency update
if err := runHelmDepUpdate(chartPath); err != nil {
logger.Fatalf("Error running helm dependency update: %v", err)
}

// Run Helm lint
if err := runHelmLint(chartPath); err != nil {
logger.Fatalf("Helm linting failed: %v", err)
}
} else {
logger.Infof("No updates were necessary.")
}
}

func configureLogger() {
level, err := logrus.ParseLevel(logLevel)
if err != nil {
logger.Fatalf("Invalid log level: %v", err)
}
logger.SetLevel(level)

switch logFormat {
case "json":
logger.SetFormatter(&logrus.JSONFormatter{})
case "text":
logger.SetFormatter(&logrus.TextFormatter{})
default:
logger.Fatalf("Invalid log format: %s", logFormat)
}
}

func getLatestVersion(repoURL, chartName string) (string, error) {
// Check if the repoURL uses the file protocol
if strings.HasPrefix(repoURL, "file://") {
localPath := strings.TrimPrefix(repoURL, "file://")
return getLatestLocalVersion(localPath)
}

settings := cli.New()

client := getter.All(settings)
repoIndex, err := repo.NewChartRepository(&repo.Entry{
URL: repoURL,
}, client)
if err != nil {
return "", err
}

indexFile, err := repoIndex.DownloadIndexFile()
if err != nil {
return "", err
}

index, err := repo.LoadIndexFile(indexFile)
if err != nil {
return "", err
}

chartVersions, ok := index.Entries[chartName]
if !ok || len(chartVersions) == 0 {
return "", fmt.Errorf("no versions found for chart %s", chartName)
}

return chartVersions[0].Version, nil
}

func getLatestLocalVersion(localPath string) (string, error) {
chart, err := loader.LoadDir(localPath)
if err != nil {
return "", fmt.Errorf("failed to load chart from %s: %v", localPath, err)
}
return chart.Metadata.Version, nil
}

// LoggerWriter is a custom writer that writes to the logger
type LoggerWriter struct {
logger *logrus.Logger
}

func (lw *LoggerWriter) Write(p []byte) (n int, err error) {
lw.logger.Info(string(p))
return len(p), nil
}

func runHelmDepUpdate(chartPath string) error {
chartDir := filepath.Dir(chartPath)
// Create a custom writer for the logger
loggerWriter := &LoggerWriter{logger: logger}

man := &downloader.Manager{
Out: loggerWriter,
Verify: downloader.VerifyIfPossible,
SkipUpdate: false,
ChartPath: chartDir,
Getters: getter.All(cli.New()),
RepositoryCache: os.TempDir(),
}

return man.Update()
}

func runHelmLint(chartPath string) error {
chartDir := filepath.Dir(chartPath)

linter := support.Linter{ChartDir: chartDir}
rules.Chartfile(&linter)
rules.Dependencies(&linter)

if len(linter.Messages) > 0 {
for _, msg := range linter.Messages {
if msg.Severity == support.ErrorSev {
return fmt.Errorf("linting error: %s", msg)
}
logger.Warnf("lint warning: %s", msg.Err)
}
}

return nil
}
Loading

0 comments on commit e0ebf9b

Please sign in to comment.