/
main.go
139 lines (117 loc) · 4.35 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
package core
import (
"context"
"fmt"
"os"
"github.com/go-logr/logr"
"github.com/spf13/pflag"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"sigs.k8s.io/yaml"
)
// NOTE some limitations of this approach:
// - comments go poof because of the YAML->JSON->YAML conversion. Don't think we can save those without a pure YAML
// workflow. Hopefully matters less for user values.yamls, but I expect at least some have their own comments that
// would have to be re-added manually.
// - existing and new keys are simply alphabetized at some point. preserving those would presumably require keeping
// track of indices for each and finding some manipulation tool that does preserve them. structs probably do this
// but we're not operating on structs.
type Config struct {
// SourceChart indicates whether the values.yaml to migrate comes from the "kong" or "ingress" chart
SourceChart string
// InputFile is the values.yaml filename to migrate.
InputFile string
// OutputFormat is the output format.
OutputFormat string
flagSet *pflag.FlagSet
}
func (c *Config) FlagSet() *pflag.FlagSet {
flagSet := pflag.NewFlagSet("", pflag.ContinueOnError)
flagSet.StringVarP(&c.SourceChart, "source-chart", "s", "kong", `The chart of the original values.yaml, either "kong" or "ingress".`)
flagSet.StringVarP(&c.InputFile, "file", "f", "./values.yaml", `Path to the values.yaml to transform.`)
flagSet.StringVar(&c.OutputFormat, "output-format", "yaml", `Output format, either "yaml" (default) or "json"`)
c.flagSet = flagSet
return flagSet
}
const (
kongChart = "kong"
ingressChart = "ingress"
)
// RunOut runs Run and prints its result to stdout.
func RunOut(ctx context.Context, c *Config, logger logr.Logger) error {
output, err := Run(ctx, c, logger)
if err != nil {
return err
}
fmt.Printf("%s", string(output))
return nil
}
// Run takes a configuration string and returns a migrated configuration string.
func Run(_ context.Context, c *Config, logger logr.Logger) (string, error) {
raw, err := os.ReadFile(c.InputFile)
if err != nil {
return "", fmt.Errorf("could not read %s: %w", c.InputFile, err)
}
orig, err := yaml.YAMLToJSON(raw)
if err != nil {
return "", fmt.Errorf("could not parse input values.yaml YAML into JSON: %w", err)
}
// Keep a copy of the original to diff later.
transformed := orig
var remaps mapFunc
switch originChart := c.SourceChart; originChart {
case ingressChart:
remaps = getIngressKeys
case kongChart:
remaps = getKongKeys
default:
return "", fmt.Errorf("unknown source chart: %s", originChart)
}
for start, end := range remaps() {
var found bool
transformed, found, err = Move(start, end, transformed)
if err != nil {
logger.Error(err, "migration failed")
}
if found {
// not immediately clear why, but attempting to move AND delete within Move (the contents of Delete originally
// followed the sjson.SetBytes() call and error check) resulted in it deleting both the old and new key.
// Presumably something about how it addresses the values internally. Returning and then deleting avoids this,
// since we have a new []byte to work with.
transformed, err = Delete(start, transformed)
if err != nil {
logger.Error(err, "cleanup failed")
}
}
}
if c.OutputFormat == "json" {
return string(transformed), nil
}
yamlOut, err := yaml.JSONToYAML(transformed)
if err != nil {
return "", fmt.Errorf("could not convert back to YAML: %w", err)
}
return string(yamlOut), nil
}
// Move takes a start and end JSON path string and a document, and returns a document with the start path moved to the
// end path. It also returns a boolean indicating if the start path is not present, in which case the input document is
// returned unmodified.
func Move(start, end string, doc []byte) ([]byte, bool, error) {
found := gjson.GetBytes(doc, start)
if !found.Exists() {
return doc, false, nil
}
result, err := sjson.SetBytes(doc, end, found.Value())
if err != nil {
return doc, true, fmt.Errorf("could not inject %s at %s: %w", start, end, err)
}
return result, true, nil
}
// Delete takes a JSON path and JSON document and returns a JSON document with the input path deleted.
func Delete(key string, doc []byte) ([]byte, error) {
result, err := sjson.DeleteBytes(doc, key)
if err != nil {
return doc, fmt.Errorf("could not delete old content at %s: %w", key, err)
}
return result, nil
}