Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add --force-yaml-string-quotation forcing string quotation for rendered YAML #731

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ADOPTERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
* GitLab
* Grafana Labs
* Yelp
* Ankorstore
2 changes: 2 additions & 0 deletions cmd/tk/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func exportCmd() *cli.Command {
vars := workflowFlags(cmd.Flags())
getJsonnetOpts := jsonnetFlags(cmd.Flags())
getLabelSelector := labelSelectorFlag(cmd.Flags())
getYAMLOpts := yamlStyleFlags(cmd.Flags())

recursive := cmd.Flags().BoolP("recursive", "r", false, "Look recursively for Tanka environments")

Expand All @@ -62,6 +63,7 @@ func exportCmd() *cli.Command {
Merge: *merge,
Opts: tanka.Opts{
JsonnetOpts: getJsonnetOpts(),
YamlOpts: getYAMLOpts(),
Filters: filters,
Name: vars.name,
},
Expand Down
10 changes: 10 additions & 0 deletions cmd/tk/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ func workflowFlags(fs *pflag.FlagSet) *workflowFlagVars {
return &v
}

func yamlStyleFlags(fs *pflag.FlagSet) func() tanka.YAMLOpts {
forceStringQuotation := fs.Bool("force-yaml-string-quotation", false, "enforce YAML quotation for strings")

return func() tanka.YAMLOpts {
return tanka.YAMLOpts{
ForceStringQuotation: *forceStringQuotation,
}
}
}

func labelSelectorFlag(fs *pflag.FlagSet) func() labels.Selector {
labelSelector := fs.StringP("selector", "l", "", "Label selector. Uses the same syntax as kubectl does")

Expand Down
10 changes: 7 additions & 3 deletions cmd/tk/workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ func showCmd() *cli.Command {

vars := workflowFlags(cmd.Flags())
getJsonnetOpts := jsonnetFlags(cmd.Flags())
getYAMLOpts := yamlStyleFlags(cmd.Flags())

cmd.Run = func(cmd *cli.Command, args []string) error {
if !interactive && !*allowRedirect {
Expand All @@ -212,17 +213,20 @@ Otherwise run tk show --dangerous-allow-redirect to bypass this check.`)
return err
}

pretty, err := tanka.Show(args[0], tanka.Opts{
opts := tanka.Opts{
JsonnetOpts: getJsonnetOpts(),
YamlOpts: getYAMLOpts(),
Filters: filters,
Name: vars.name,
})
}

pretty, err := tanka.Show(args[0], opts)

if err != nil {
return err
}

return pageln(pretty.String())
return pageln(pretty.String(opts.YamlOpts.ForceStringQuotation))
}
return cmd
}
2 changes: 1 addition & 1 deletion pkg/kubernetes/client/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func (k Kubectl) Apply(data manifest.List, opts ApplyOpts) error {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

cmd.Stdin = strings.NewReader(data.String())
cmd.Stdin = strings.NewReader(data.String(false))

return cmd.Run()
}
2 changes: 1 addition & 1 deletion pkg/kubernetes/client/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (k Kubectl) diff(data manifest.List, serverSide bool) (*string, error) {
cmd.Stdout = &raw
}
cmd.Stderr = &fw
cmd.Stdin = strings.NewReader(data.String())
cmd.Stdin = strings.NewReader(data.String(false))
err := cmd.Run()
if diffErr := parseDiffErr(err, fw.buf, k.Info().ClientVersion); diffErr != nil {
return nil, diffErr
Expand Down
2 changes: 1 addition & 1 deletion pkg/kubernetes/client/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (k Kubectl) GetByLabels(namespace, kind string, labels map[string]string) (
func (k Kubectl) GetByState(data manifest.List, opts GetByStateOpts) (manifest.List, error) {
list, err := k.get("", "", []string{"-f", "-"}, getOpts{
ignoreNotFound: opts.IgnoreNotFound,
stdin: data.String(),
stdin: data.String(false),
})
if err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion pkg/kubernetes/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ func StaticDiffer(create bool) Differ {
return func(state manifest.List) (*string, error) {
s := ""
for _, m := range state {
is, should := m.String(), ""
is, should := m.String(false), ""
if create {
is, should = should, is
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/kubernetes/manifest/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func (s *SchemaError) Error() string {

if s.Manifest != nil {
msg += bluef("\nPlease check below object:\n")
msg += SampleString(s.Manifest.String()).Indent(2)
msg += SampleString(s.Manifest.String(false)).Indent(2)
}

return msg
Expand Down
36 changes: 31 additions & 5 deletions pkg/kubernetes/manifest/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,19 @@ import (
"github.com/Masterminds/sprig/v3"
"github.com/pkg/errors"
"github.com/stretchr/objx"
yaml "gopkg.in/yaml.v2"
yaml "gopkg.in/yaml.v3"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's going to be a blocker for us 😞. v3 has major formatting differences from v2, and there's no way to configure it (see #607 (comment)). For anyone using GitOps, this upgrade is very troublesome because it means that most Kubernetes resources will be redeployed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Damn, didn't think about this, thought this had been resolved already. Do you consider switching to the fork the kustomize folks are using right now? With the v2 API, we cannot have such functionalities

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can maintain the same indentations as we currently have, and it supports new features such as this, then I think it'd be a good option

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So - the short version of this is that we cannot, I'll check if I can come up with another way, maybe a feature flag like --use-yaml-v3?

)

func yamlForceStringQuotationRecursivePatch(n *yaml.Node) {
if n.Tag == "!!str" {
n.Style = yaml.DoubleQuotedStyle
}
for _, cNode := range n.Content {
// for all child nodes
yamlForceStringQuotationRecursivePatch(cNode)
}
}

// Manifest represents a Kubernetes API object. The fields `apiVersion` and
// `kind` are required, `metadata.name` should be present as well
type Manifest map[string]interface{}
Expand All @@ -31,8 +41,17 @@ func NewFromObj(raw objx.Map) (Manifest, error) {
}

// String returns the Manifest in yaml representation
func (m Manifest) String() string {
y, err := yaml.Marshal(m)
func (m Manifest) String(forceQuotedStrings bool) string {

yamlNode := &yaml.Node{}
if err := yamlNode.Encode(m); err != nil {
panic(errors.Wrap(err, "converting manifest to yaml.Node"))
}
if forceQuotedStrings {
yamlForceStringQuotationRecursivePatch(yamlNode)
}

y, err := yaml.Marshal(yamlNode)
if err != nil {
// this should never go wrong in normal operations
panic(errors.Wrap(err, "formatting manifest"))
Expand Down Expand Up @@ -262,12 +281,19 @@ type List []Manifest

// String returns the List as a yaml stream. In case of an error, it is
// returned as a string instead.
func (m List) String() string {
func (m List) String(forceQuotedStrings bool) string {
buf := bytes.Buffer{}
enc := yaml.NewEncoder(&buf)

for _, d := range m {
if err := enc.Encode(d); err != nil {
yamlNode := &yaml.Node{}
if err := yamlNode.Encode(d); err != nil {
panic(errors.Wrap(err, "converting manifest to yaml.Node"))
}
if forceQuotedStrings {
yamlForceStringQuotationRecursivePatch(yamlNode)
}
if err := enc.Encode(yamlNode); err != nil {
// This should never happen in normal operations
panic(errors.Wrap(err, "formatting manifests"))
}
Expand Down
87 changes: 69 additions & 18 deletions pkg/kubernetes/manifest/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,79 @@ import (
"gopkg.in/yaml.v3"
)

// UnmarshalExpect defines the expected Unmarshal result. Types are very
// important here, only nil, float64, bool, string, map[string]interface{} and
// []interface{} may exist.
var UnmarshalExpect = Manifest{
"apiVersion": string("apps/v1"),
"kind": string("Deployment"),
"metadata": map[string]interface{}{
"name": string("MyDeployment"),
},
"spec": map[string]interface{}{
"replicas": float64(3),
"template": map[string]interface{}{
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": string("nginx"),
"image": string("nginx:1.14.2"),
var (
// UnmarshalExpect defines the expected Unmarshal result. Types are very
// important here, only nil, float64, bool, string, map[string]interface{} and
// []interface{} may exist.
UnmarshalExpect = Manifest{
"apiVersion": string("apps/v1"),
"kind": string("Deployment"),
"metadata": map[string]interface{}{
"name": string("MyDeployment"),
},
"spec": map[string]interface{}{
"replicas": float64(3),
"template": map[string]interface{}{
"spec": map[string]interface{}{
"containers": []interface{}{
map[string]interface{}{
"name": string("nginx"),
"image": string("nginx:1.14.2"),
},
},
},
},
},
},
}

// Manifest defines the manifest we're comparing the marshal results below to.
SubstituteManifest = Manifest{
"apiVersion": string("test/v1"),
"kind": string("Test"),
"metadata": map[string]interface{}{
"name": string("test"),
},
"spec": map[string]interface{}{
"integer": float64(3),
"string": string("${TEST_ENV}"),
},
}

// SubstituteManifestString defines the expected Marshal result when using String(forceQuotedStrings=true).
SubstituteManifestString = `apiVersion: test/v1
kind: Test
metadata:
name: test
spec:
integer: 3
string: ${TEST_ENV}
`

// SubstituteManifestQuotedString defines the expected Marshal result when using String(forceQuotedStrings=falsetrue).
SubstituteManifestQuotedString = `"apiVersion": "test/v1"
"kind": "Test"
"metadata":
"name": "test"
"spec":
"integer": 3
"string": "${TEST_ENV}"
`
)

func TestMarshalString(t *testing.T) {
m := SubstituteManifest.String(false)

if s := cmp.Diff(SubstituteManifestString, m); s != "" {
t.Error(s)
}
}

func TestMarshalQuotedString(t *testing.T) {
m := SubstituteManifest.String(true)

if s := cmp.Diff(SubstituteManifestQuotedString, m); s != "" {
t.Error(s)
}
}

func TestUnmarshalJSON(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions pkg/kubernetes/subsetdiff.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,10 @@ func subsetDiff(c client.Client, m manifest.Manifest) (*difference, error) {
return nil, errors.Wrap(err, "getting state from cluster")
}

should := m.String()
should := m.String(false)

sub := subset(m, rawIs)
is := manifest.Manifest(sub).String()
is := manifest.Manifest(sub).String(false)
if is == "{}\n" {
is = ""
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/tanka/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func ExportEnvironments(envs []*v1alpha1.Environment, to string, opts *ExportEnv
}

// Write manifest
data := m.String()
data := m.String(opts.Opts.YamlOpts.ForceStringQuotation)
if err := writeExportFile(path, []byte(data)); err != nil {
return err
}
Expand Down
5 changes: 5 additions & 0 deletions pkg/tanka/tanka.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,14 @@ import (

type JsonnetOpts = jsonnet.Opts

type YAMLOpts struct {
ForceStringQuotation bool
}

// Opts specify general, optional properties that apply to all actions
type Opts struct {
JsonnetOpts
YamlOpts YAMLOpts

// Filters are used to optionally select a subset of the resources
Filters process.Matchers
Expand Down