Compare commits

...

10 commits

9 changed files with 407 additions and 19 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
helmfile2argo

View file

@ -0,0 +1 @@
GPLv3

View file

@ -3,6 +3,8 @@
| :exclamation: | This is an extremely naiive implementation and might be not what you really want, but it's better than nothing. | | :exclamation: | This is an extremely naiive implementation and might be not what you really want, but it's better than nothing. |
|----------------|:------------------------| |----------------|:------------------------|
The actual intent of this is to create a basic structure for you to refine, when you currently have helmfile and want to move to argoCD.
## How it works ## How it works
- It strips **ALL** templating from the yaml file. Naiively, with a regex. - It strips **ALL** templating from the yaml file. Naiively, with a regex.
@ -15,3 +17,8 @@
- "Magic". For example, it won't replace hardcoded https://path.to/some/tarball-helm-chart.tgz with something that is argo-compatible. Point to a real helm repo, an oci repo or a git repo, argo can deal with those. - "Magic". For example, it won't replace hardcoded https://path.to/some/tarball-helm-chart.tgz with something that is argo-compatible. Point to a real helm repo, an oci repo or a git repo, argo can deal with those.
- Multiple environments. There's not an opinionated "this is how you do it in argo" for that, so you need to work out what to do there. You can pass the environment you want to use in, and if you're using "environment" values, they'll be included in the application object for you. - Multiple environments. There's not an opinionated "this is how you do it in argo" for that, so you need to work out what to do there. You can pass the environment you want to use in, and if you're using "environment" values, they'll be included in the application object for you.
- The same as helmfile, sometimes. For example, it will only descend one level deep of helmfile references and values in the sub helmfiles might leak upwards.
## Context
This was written on trains and in cafes in my personal time, don't expect too much from it, use it if it's useful to you.

View file

@ -1,15 +1,127 @@
/* /*
Copyright © 2026 NAME HERE <EMAIL ADDRESS> Copyright © 2026 NAME HERE <EMAIL ADDRESS>
*/ */
package cmd package cmd
import ( import (
"errors"
"fmt" "fmt"
"maps"
"os"
"path/filepath"
"regexp"
"slices"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/yookoala/realpath"
"gopkg.in/yaml.v3"
"internal/fileflag"
) )
type helmfileStructure struct {
Repositories []helmfileRepository `json:"repositories"`
Releases []helmfileHelmRelease `json:"releases"`
HelmFiles []helmfileHemfileRef `json:"helmfiles"`
Environments map[string]interface{} `json:"environments"`
}
type helmfileRepository struct {
Name string `json:"name"`
Url string `json:"url"`
}
type helmfileHelmRelease struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Enabled bool `json:"enabled"`
Installed bool `json:"installed"`
Labels string `json:"labels"`
Chart string `json:"chart"`
Version string `json:"version"`
Values []map[string]interface{} `json:"values"`
}
type helmfileHemfileRef struct {
Path string `json:"path"`
Values []map[string]interface{} `json:"values"`
}
type argoProjectMeta struct {
Name string `json:"name"`
}
type argoProjectDestination struct {
Namespace string `json:"namespace"`
Server string `json:"server"`
}
type argoResourceWhitelistItem struct {
Group string `json:"group"`
Kind string `json:"kind"`
}
type argoProjectSpec struct {
SourceRepos []string `json:"sourceRepos"`
Destinations []argoProjectDestination `json:"Destinations"`
ClusterResourceWhitelist []argoResourceWhitelistItem `json:"clusterResourceWhitelist"`
}
type argoProject struct {
ApiVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Metadata argoProjectMeta `json:"metadata"`
Spec argoProjectSpec `json:"spec"`
}
type argoAppMeta struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
}
type argoAppSpecSourceHelm struct {
ReleaseName string `releaseName`
ValuesObject map[string]interface{} `valuesObject`
}
type argoAppSpecSource struct {
Chart string `json:"chart"`
RepoURL string `json:"repoURL"`
targetRevision string `json:"targetRevision"`
Helm argoAppSpecSourceHelm `json:"helm"`
}
type argoAppSpecDestination struct {
Server string `json:"server"`
Namespace string `json:"namespace"`
}
type argoAppSpec struct {
Project string `json:"project"`
Source argoAppSpecSource `json:"source"`
Destination argoAppSpecDestination `json: "destination"`
}
type argoApp struct {
ApiVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Metadata argoAppMeta `json:"metadata"`
Spec argoAppSpec `json:"spec"`
}
var flagInputYaml fileflag.InputFileFlag = "helmfile.yaml"
var singleFile bool = false
var outputDir string = "-" //TODO: actually handle files
var appNamespace string = "argocd"
var projectName string = "helmfile-imported"
var createProject bool = true
var serverURL string = "https://kubernetes.default.svc"
var recursiveConvert bool = false
var loadEnvironmentValues string = ""
var reposFromHelmChart []helmfileRepository
var environmentValues map[string]interface{}
var filesWrittenTo []string
// convertCmd represents the convert command // convertCmd represents the convert command
var convertCmd = &cobra.Command{ var convertCmd = &cobra.Command{
Use: "convert", Use: "convert",
@ -18,10 +130,231 @@ var convertCmd = &cobra.Command{
a set of .yaml files describing argocd Applications that are built from the a set of .yaml files describing argocd Applications that are built from the
releases section.`, releases section.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Println("convert called") helmfileContents := parseHelmfile(string(flagInputYaml), nil)
if recursiveConvert && len(helmfileContents.HelmFiles) > 0 {
for i := 0; i <= len(helmfileContents.HelmFiles)-1; i++ {
fmt.Print("Merging in ")
fmt.Println(helmfileContents.HelmFiles[i].Path)
mergeIn := parseHelmfile(string(helmfileContents.HelmFiles[i].Path), helmfileContents.HelmFiles[i].Values)
helmfileContents.Repositories = append(helmfileContents.Repositories, mergeIn.Repositories[:]...)
helmfileContents.Releases = append(helmfileContents.Releases, mergeIn.Releases[:]...)
maps.Copy(helmfileContents.Environments,mergeIn.Environments)
}
}
fmt.Println("converting file:", flagInputYaml)
if (loadEnvironmentValues != "") {
environmentMap, ok := helmfileContents.Environments[loadEnvironmentValues].(map[string]interface{})
if !ok {
fmt.Println("Error, no enviroments defined in helmfile, but environment passed in command.")
os.Exit(1)
}
var valueFilesMap = environmentMap["values"].([]interface{})
var valueFiles []string
for i := range valueFilesMap {
valueFiles = append(valueFiles, valueFilesMap[i].(string))
}
environmentValues = fetchEnvironmentValues(valueFiles)
}
if createProject && (len(helmfileContents.Repositories) > 0) {
outputProject := baseProject(projectName)
for i := 0; i <= len(helmfileContents.Repositories)-1; i++ {
outputProject.Spec.SourceRepos = append(outputProject.Spec.SourceRepos, helmfileContents.Repositories[i].Url)
}
outputProjectYaml, _ := yaml.Marshal(outputProject)
var outputFilename = "project-"+projectName+".yaml"
if singleFile {
outputFilename = "helmfile2argo.yaml"
}
outputYamlFile(outputProjectYaml, outputFilename)
}
reposFromHelmChart = helmfileContents.Repositories
if len(helmfileContents.Releases) > 0 {
for i := 0; i <= len(helmfileContents.Releases)-1; i++ {
thisRelease := helmfileContents.Releases[i]
outputApp := baseArgoApp(thisRelease.Name)
outputApp.Spec.Project = projectName
if thisRelease.Chart[0:4] == "http" {
outputApp.Spec.Source.Chart = "UNSUPPORTED_BY_ARGO"
outputApp.Spec.Source.RepoURL = thisRelease.Chart
} else {
var chartmatcher = regexp.MustCompile(`(^[^/]*)/(.*)`)
chart := chartmatcher.ReplaceAllString(thisRelease.Chart, `$2`)
repo := chartmatcher.ReplaceAllString(thisRelease.Chart, `$1`)
outputApp.Spec.Source.Chart = chart
var err error
err, outputApp.Spec.Source.RepoURL = repoURLbyName(repo)
if err != nil {
outputApp.Spec.Source.RepoURL = "UNDEFINED"
}
}
outputApp.Spec.Source.Helm.ReleaseName = thisRelease.Name
if (loadEnvironmentValues != "") {
//flattened := flattenHelmfileWeirdValues(thisRelease.Values)
if (environmentValues == nil) {
outputApp.Spec.Source.Helm.ValuesObject = flattenHelmfileWeirdValues(thisRelease.Values)
} else if (thisRelease.Values == nil) {
outputApp.Spec.Source.Helm.ValuesObject = environmentValues
} else {
merged := environmentValues
maps.Copy(merged, flattenHelmfileWeirdValues(thisRelease.Values))
outputApp.Spec.Source.Helm.ValuesObject = merged
}
} else {
outputApp.Spec.Source.Helm.ValuesObject = flattenHelmfileWeirdValues(thisRelease.Values)
}
outputApp.Spec.Destination.Server = serverURL
outputApp.Spec.Destination.Namespace = thisRelease.Namespace
outputAppYaml, _ := yaml.Marshal(outputApp)
var outputFilename = "app-"+thisRelease.Name+".yaml"
if singleFile {
outputFilename = "helmfile2argo.yaml"
}
outputYamlFile(outputAppYaml,outputFilename)
}
}
}, },
} }
func flattenHelmfileWeirdValues(hfv []map[string]interface{}) map[string]interface{} {
var nv map[string]interface{}
for i := range hfv {
if i == 0 {
nv = hfv[i]
} else {
maps.Copy(nv,hfv[i])
}
}
return nv
}
func parseHelmfile(helmfileFilename string, passedValues []map[string]interface{}) helmfileStructure {
var finalPath = helmfileFilename
if helmfileFilename != string(flagInputYaml) {
absolutePath, err := realpath.Realpath(string(flagInputYaml))
basePath := filepath.Dir(absolutePath)
finalPath, err = realpath.Realpath(basePath + string(os.PathSeparator) + helmfileFilename)
if err != nil {
panic(err)
}
}
fmt.Println("parsing file:", finalPath)
yamlFile, err := os.ReadFile(finalPath)
if err != nil {
fmt.Printf("yamlFile.Get err #%v ", err)
}
templatingMatch := regexp.MustCompile(`{{[^{}]*}}`)
passOneRemoveTemplating := templatingMatch.ReplaceAllString(string(yamlFile), `UNDEFINED`)
passTwoRemoveTemplating := templatingMatch.ReplaceAllString(passOneRemoveTemplating, `UNDEFINED`)
multidocMatch := regexp.MustCompile(`\n---\n`)
passThreeRemoveMultiDoc := multidocMatch.ReplaceAllString(passTwoRemoveTemplating, "\n")
helmfileContents := &helmfileStructure{}
err = yaml.Unmarshal([]byte(passThreeRemoveMultiDoc), helmfileContents)
if err != nil {
fmt.Printf("Unmarshal: %v", err)
}
if passedValues != nil {
for i := 0; i <= len(helmfileContents.Releases)-1; i++ {
helmfileContents.Releases[i].Values = append(helmfileContents.Releases[i].Values, passedValues...)
}
}
return *helmfileContents
}
func fetchEnvironmentValues(files []string) map[string]interface{} {
var allValues map[string]interface{}
for i := 0; i <= len(files)-1; i++ {
absolutePath, err := realpath.Realpath(string(flagInputYaml))
basePath := filepath.Dir(absolutePath)
finalPath, err := realpath.Realpath(basePath + string(os.PathSeparator) + files[i])
fmt.Print("Loading values from ")
fmt.Println(finalPath)
if err != nil {
panic(err)
}
yamlFile, err := os.ReadFile(finalPath)
if err != nil {
panic(err)
}
var thisFilesValues map[string]interface{}
if err := yaml.Unmarshal(yamlFile, &thisFilesValues); err != nil {
panic(err)
}
fmt.Println(thisFilesValues)
if i == 0 {
allValues = thisFilesValues
} else {
maps.Copy(allValues, thisFilesValues)
}
}
fmt.Println(allValues)
return allValues
}
func fetchRepos(helmfileFilename string) []helmfileRepository {
return nil
}
func repoURLbyName(repoName string) (error, string) {
for i := 0; i <= len(reposFromHelmChart)-1; i++ {
if reposFromHelmChart[i].Name == repoName {
return nil, reposFromHelmChart[i].Url
}
}
return errors.New("No repo by that name found!"), ""
}
func outputString(data string, filename string) {
if string(outputDir) == "-" {
fmt.Println(data)
} else {
var mode int
if slices.Contains(filesWrittenTo,filename) {
mode = os.O_APPEND|os.O_CREATE|os.O_WRONLY
} else {
filesWrittenTo = append(filesWrittenTo, filename)
mode = os.O_TRUNC|os.O_CREATE|os.O_WRONLY
}
f, err := os.OpenFile(outputDir+string(os.PathSeparator)+filename, mode, 0644)
if err != nil {
panic(err)
}
defer f.Close()
if _, err := f.WriteString(data); err != nil {
panic(err)
}
}
}
func outputYamlFile(bytes []byte, filename string) {
if singleFile || (string(outputDir) == "-") {
outputString("---\n", filename)
}
outputString(string(bytes)+"\n", filename)
}
func baseProject(name string) argoProject {
outputProject := argoProject{}
outputProject.ApiVersion = "argoproj.io/v1alpha1"
outputProject.Kind = "AppProject"
outputProject.Spec.Destinations = append(outputProject.Spec.Destinations, argoProjectDestination{Namespace: "*", Server: "*"})
outputProject.Spec.ClusterResourceWhitelist = append(outputProject.Spec.ClusterResourceWhitelist, argoResourceWhitelistItem{Group: "*", Kind: "*"})
outputProject.Metadata.Name = name
return outputProject
}
func baseArgoApp(name string) argoApp {
outputApp := argoApp{}
outputApp.ApiVersion = "argoproj.io/v1alpha1"
outputApp.Kind = "Application"
outputApp.Metadata.Namespace = appNamespace
outputApp.Metadata.Name = name
return outputApp
}
func init() { func init() {
rootCmd.AddCommand(convertCmd) rootCmd.AddCommand(convertCmd)
@ -34,4 +367,14 @@ func init() {
// Cobra supports local flags which will only run when this command // Cobra supports local flags which will only run when this command
// is called directly, e.g.: // is called directly, e.g.:
// convertCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") // convertCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
convertCmd.Flags().VarP(&flagInputYaml, "input", "i", `helmfile to convert`)
convertCmd.Flags().BoolVarP(&singleFile, "single-file", "1", false, `single output file (always true if output is "-")`)
convertCmd.Flags().StringVarP(&outputDir, "output", "o", "-", `output folder or "-" for stdout`)
convertCmd.Flags().StringVarP(&appNamespace, "appnamespace", "a", "argocd", `namespace for application objects`)
convertCmd.Flags().StringVarP(&projectName, "projectname", "p", "helmfile-imported", `project name for all apps`)
convertCmd.Flags().BoolVarP(&createProject, "create-project", "c", true, `create project with above projectname and all repositories from helmfile`)
convertCmd.Flags().StringVarP(&serverURL, "server-name", "n", "https://kubernetes.default.svc", `spec.destination.server value in the application objects`)
convertCmd.Flags().BoolVarP(&recursiveConvert, "recursive", "r", false, `recurse into sub helmfiles`)
convertCmd.Flags().StringVarP(&loadEnvironmentValues, "load-environment-values", "e", "", `set to an environment name to load environment-based values in the top-level helmfile`)
} }

View file

@ -1,13 +1,13 @@
/* /*
Copyright © 2026 NAME HERE <EMAIL ADDRESS> Copyright © 2026 Martyn Ranyard <m@rtyn.berlin>
GPLv3 licensed
*/ */
package cmd package cmd
import ( import (
"os"
"fmt"
"errors" "errors"
"fmt"
"os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -16,7 +16,7 @@ import (
var ( var (
cfgFile string cfgFile string
// rootCmd represents the base command when called without any subcommands // rootCmd represents the base command when called without any subcommands
rootCmd = &cobra.Command{ rootCmd = &cobra.Command{
Use: "helmfile2argo", Use: "helmfile2argo",
Short: "Convert a helmfile to a set of argocd applications", Short: "Convert a helmfile to a set of argocd applications",
@ -68,5 +68,3 @@ func init() {
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.helmfile2argo/config)") rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.helmfile2argo/config)")
initializeConfig(rootCmd) initializeConfig(rootCmd)
} }

21
go.mod
View file

@ -3,19 +3,32 @@ module git.martyn.berlin/martyn/helmfile2argo
go 1.26.2 go 1.26.2
require ( require (
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
github.com/yookoala/realpath v1.0.0
gopkg.in/yaml.v3 v3.0.1
internal/fileflag v1.0.0
)
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/sys v0.29.0 // indirect golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.28.0 // indirect golang.org/x/text v0.36.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
) )
replace internal/fileflag => ./internal/fileflag

33
go.sum
View file

@ -1,12 +1,29 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
@ -23,12 +40,20 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/yookoala/realpath v1.0.0 h1:7OA9pj4FZd+oZDsyvXWQvjn5oBdcHRTV44PpdMSuImQ=
github.com/yookoala/realpath v1.0.0/go.mod h1:gJJMA9wuX7AcqLy1+ffPatSCySA1FQ2S8Ya9AIoYBpE=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

Binary file not shown.

View file

@ -1,6 +1,6 @@
/* /*
Copyright © 2026 NAME HERE <EMAIL ADDRESS> Copyright © 2026 Martyn Ranyard <m@rtyn.berlin>
GPLv3 licensed
*/ */
package main package main