Skip to content

Commit

Permalink
Support snapshot format v3
Browse files Browse the repository at this point in the history
- v3 format support to keep YAML property orders and comments using kustomize/kyaml
- refactoring charts packages. add api/v1alpha1 packages to define chartsnap's specs: e.g. testspec in values.yaml, snapshot file header and unknown resource.
  • Loading branch information
jlandowner committed Apr 29, 2024
1 parent f40c890 commit 677c95f
Show file tree
Hide file tree
Showing 46 changed files with 4,162 additions and 312 deletions.
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ require (
golang.org/x/sync v0.7.0
k8s.io/apimachinery v0.29.3
sigs.k8s.io/controller-runtime v0.17.3
sigs.k8s.io/kustomize/kyaml v0.17.0
sigs.k8s.io/yaml v1.4.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.12.0 // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
Expand All @@ -42,9 +44,11 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/oauth2 v0.19.0 // indirect
Expand Down
16 changes: 14 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk=
github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
Expand All @@ -12,6 +13,8 @@ github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
Expand Down Expand Up @@ -66,6 +69,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
Expand All @@ -76,8 +81,9 @@ github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeB
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
Expand All @@ -90,14 +96,18 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
Expand Down Expand Up @@ -180,6 +190,8 @@ sigs.k8s.io/controller-runtime v0.17.3 h1:65QmN7r3FWgTxDMz9fvGnO1kbf2nu+acg9p2R9
sigs.k8s.io/controller-runtime v0.17.3/go.mod h1:N0jpP5Lo7lMTF9aL56Z/B2oWBJjey6StQM0jRbKQXtY=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/kustomize/kyaml v0.17.0 h1:G2bWs03V9Ur2PinHLzTUJ8Ded+30SzXZKiO92SRDs3c=
sigs.k8s.io/kustomize/kyaml v0.17.0/go.mod h1:6lxkYF1Cv9Ic8g/N7I86cvxNc5iinUo/P2vKsHNmpyE=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
Expand Down
50 changes: 19 additions & 31 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import (
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"

"github.com/jlandowner/helm-chartsnap/pkg/api/v1alpha1"
"github.com/jlandowner/helm-chartsnap/pkg/charts"
"github.com/jlandowner/helm-chartsnap/pkg/snap"
)

var (
Expand All @@ -38,6 +38,7 @@ type option struct {
Parallelism int
ConfigFile string
LegacySnapshot bool
SnapshotVersion string

// Below properties are the same as helm global options
// They are passed to the plugin as environment variables
Expand Down Expand Up @@ -76,6 +77,16 @@ func (o *option) OK() string {
return "matched"
}

// compatibility for --legacy-snapshot flag
func (o *option) snapshotVersion() string {
// use v1 snapshot format if legacy snapshot format is enabled
if o.LegacySnapshot {
return charts.SnapshotVersionV1
} else {
return o.SnapshotVersion
}
}

func main() {
rootCmd := &cobra.Command{
Use: "chartsnap -c CHART",
Expand Down Expand Up @@ -171,6 +182,7 @@ MIT 2023 jlandowner/helm-chartsnap
panic(err)
}
rootCmd.PersistentFlags().BoolVar(&o.LegacySnapshot, "legacy-snapshot", false, "use toml-based legacy snapshot format")
rootCmd.PersistentFlags().StringVar(&o.SnapshotVersion, "snapshot-version", "", "use a specific snapshot version. v1, v2, v3 are supported. (default: latest)")

if err := rootCmd.Execute(); err != nil {
slog.New(slogHandler()).Error(err.Error())
Expand All @@ -197,8 +209,8 @@ func prerun(cmd *cobra.Command, args []string) error {
return nil
}

func loadSnapshotConfig(file string, cfg *charts.SnapshotConfig) error {
err := charts.LoadSnapshotConfig(file, cfg)
func loadSnapshotConfig(file string, cfg *v1alpha1.SnapshotConfig) error {
err := v1alpha1.FromFile(file, cfg)
if err != nil && !os.IsNotExist(err) {
if o.FailFast {
return fmt.Errorf("failed to load snapshot config: %w", err)
Expand All @@ -222,7 +234,7 @@ func run(cmd *cobra.Command, args []string) error {
}
}

var cfg charts.SnapshotConfig
var cfg v1alpha1.SnapshotConfig
if _, err := os.Stat(o.ConfigFile); err == nil {
if err := loadSnapshotConfig(o.ConfigFile, &cfg); err != nil {
return err
Expand Down Expand Up @@ -305,38 +317,14 @@ func run(cmd *cobra.Command, args []string) error {
snapshotFilePath = charts.DefaultSnapshotFilePath(ht.Chart, ht.ValuesFile)
}

_, err := os.Stat(snapshotFilePath)
if err == nil {
log.Debug("snapshot file already exists", "path", snapshotFilePath)
} else if os.IsNotExist(err) {
log.Debug("snapshot file does not exist", "path", snapshotFilePath)
} else {
log.Error("unexpected error in snapshot file stat", "path", snapshotFilePath, "err", err)
}

if o.UpdateSnapshot {
// v1 format is multi snapshot format with encoding legacy formatted yaml
if snap.IsMultiSnapshots(snapshotFilePath) && !o.LegacySnapshot {
log.Info("WARNING: snapshot is updated to a latest format. if you want to keep a legacy format, please run with --legacy-snapshot flag", "path", snapshotFilePath)
}
err := snap.RemoveFile(snapshotFilePath)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to replace snapshot file: %w", err)
}
}

var version string
// use v1 snapshot format if legacy snapshot format is enabled
if o.LegacySnapshot {
version = charts.SnapshotVersionV1
}

snapshotter := charts.ChartSnapshotter{
HelmTemplateCmdOptions: ht,
SnapshotConfig: cfg,
SnapshotFile: snapshotFilePath,
SnapshotVersion: version,
SnapshotVersion: o.snapshotVersion(),
DiffContextLineN: o.DiffContextLineN,
UpdateSnapshot: o.UpdateSnapshot,
HeaderVersion: version,
}
result, err := snapshotter.Snap(ctx)
if err != nil {
Expand Down
87 changes: 87 additions & 0 deletions pkg/api/v1alpha1/__snapshots__/testspec_test.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
['TestSpec FromFile when loading .chartsnap.yaml should load config 1']
SnapShot = """
{
\"DynamicFields\": [
{
\"Kind\": \"Secret\",
\"APIVersion\": \"v1\",
\"Name\": \"app1-cert\",
\"JSONPath\": [
\"/data/ca.crt\",
\"/data/tls.crt\",
\"/data/tls.key\"
],
\"Base64\": true
}
]
}
"""
['TestSpec FromFile when loading invalid yaml should not load config 1']
SnapShot = """
failed to decode file 'testdata/testspec_values_invalid.yaml': yaml: line 10: could not find expected ':'"""

['TestSpec FromFile when values.yaml has testSpec should load config 1']
SnapShot = """
{
\"TestSpec\": {
\"DynamicFields\": [
{
\"Kind\": \"Secret\",
\"APIVersion\": \"v1\",
\"Name\": \"app1-cert\",
\"JSONPath\": [
\"/data/ca.crt\",
\"/data/tls.crt\",
\"/data/tls.key\"
],
\"Base64\": true
}
]
}
}
"""
['TestSpec Merge should merge dynamic fields 1']
SnapShot = """
{
\"DynamicFields\": [
{
\"Kind\": \"service\",
\"APIVersion\": \"v1\",
\"Name\": \"chartsnap-app1\",
\"JSONPath\": [
\"/spec/ports/0/targetPort\"
],
\"Base64\": false
},
{
\"Kind\": \"Pod\",
\"APIVersion\": \"v1\",
\"Name\": \"chartsnap-app1-test-connection\",
\"JSONPath\": [
\"/metadata/name\"
],
\"Base64\": false
},
{
\"Kind\": \"service\",
\"APIVersion\": \"v1\",
\"Name\": \"chartsnap-app1\",
\"JSONPath\": [
\"/spec/ports/0/targetPort\"
],
\"Base64\": false
},
{
\"Kind\": \"Service\",
\"APIVersion\": \"v1\",
\"Name\": \"chartsnap-app1\",
\"JSONPath\": [
\"/spec/ports/1/targetPort\"
],
\"Base64\": false
}
]
}
"""
40 changes: 40 additions & 0 deletions pkg/api/v1alpha1/header.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package v1alpha1

import (
"fmt"
"reflect"
"strings"
)

type Header struct {
SnapshotVersion string `header:"snapshot_version"`
}

func (h *Header) ToString() string {
return fmt.Sprintf("# chartsnap: snapshot_version=%s\n---\n", h.SnapshotVersion)
}

func ParseHeader(line string) *Header {
h := Header{}
ht := reflect.TypeOf(h)
hv := reflect.ValueOf(&h).Elem()

split := strings.Split(string([]byte(line)[1:]), " ")
for _, v := range split {
s := strings.Split(v, "=")
if len(s) != 2 {
continue
}

headerName := strings.TrimSpace(s[0])
headerValue := strings.TrimSpace(s[1])

for i := 0; i < hv.NumField(); i++ {
field := ht.Field(i)
if tag, ok := field.Tag.Lookup("header"); ok && tag == headerName {
hv.Field(i).SetString(headerValue)
}
}
}
return &h
}
66 changes: 66 additions & 0 deletions pkg/api/v1alpha1/header_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package v1alpha1

import (
"reflect"
"testing"
)

func TestHeader_ToString(t *testing.T) {
type fields struct {
Version string
SnapshotVersion string
Chart string
Values string
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "Test Header ToString",
fields: fields{
SnapshotVersion: "v3",
},
want: "# chartsnap: snapshot_version=v3\n---\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := &Header{
SnapshotVersion: tt.fields.SnapshotVersion,
}
if got := h.ToString(); got != tt.want {
t.Errorf("Header.ToString() = %v, want %v", got, tt.want)
}
})
}
}

func TestParseHeader(t *testing.T) {
type args struct {
line string
}
tests := []struct {
name string
args args
want *Header
}{
{
name: "Test ParseHeader",
args: args{
line: "# chartsnap: snapshot_version=v3\n",
},
want: &Header{
SnapshotVersion: "v3",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ParseHeader(tt.args.line); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParseHeader() = %v, want %v", got, tt.want)
}
})
}
}
Loading

0 comments on commit 677c95f

Please sign in to comment.