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

merge/replace fieldSpecs in transformer configs per strategy (#4404) #4461

Closed
wants to merge 1 commit into from
Closed

Conversation

khrisrichardson
Copy link
Contributor

@khrisrichardson khrisrichardson commented Feb 8, 2022

This PR includes a DefaultFieldSpecs() function for each of the filters utilizing fsslice, since builtinconfig is internal to api and thus not accessible from plugin/builtin. Likewise, for each of the builtin plugins using said filters, if the end-user omits a fieldSpecs attribute in the respective builtin transformer config function specification (i.e. - p.FieldSpecs == nil), the defaults will be applied by way of the aforementioned filter function related to that plugin.

Regarding backwards compatibility, as @KnVerey noted in #4404, if fieldSpecs are not specified then the builtin transformer config functions were of limited use, so the end-user was required to set the fieldSpecs attribute to achieve the desired results. Given that most of the builtin plugins did not croak on the absence of the fieldSpecs attribute, it is conceivable that there are builtin transformer config function specifications in the field that will suddenly start mutating resources with this change. Builtin transformer config functions with a fieldSpecs attribute set will continue functioning as previously.

On a forwards compatibility note, it would now be possible for two versions of kustomize to produce unequal results, which I think poses the question whether the builtin apiVersion ought not be versioned.

@k8s-ci-robot k8s-ci-robot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. labels Feb 8, 2022
@k8s-ci-robot
Copy link
Contributor

Hi @khrisrichardson. Thanks for your PR.

I'm waiting for a kubernetes-sigs member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should join the org to skip this step.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository.

@k8s-ci-robot k8s-ci-robot added the size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. label Feb 8, 2022
@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: khrisrichardson
To complete the pull request process, please assign natasha41575 after the PR has been reviewed.
You can assign the PR to them by writing /assign @natasha41575 in a comment when ready.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@yuwenma
Copy link
Contributor

yuwenma commented Feb 8, 2022

/hold

@k8s-ci-robot k8s-ci-robot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Feb 8, 2022
@natasha41575
Copy link
Contributor

/assign @yuwenma

@natasha41575
Copy link
Contributor

/ok-to-test

@k8s-ci-robot k8s-ci-robot added ok-to-test Indicates a non-member PR verified by an org member that is safe to test. and removed needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. labels Feb 8, 2022
@@ -164,3 +164,157 @@ metadata:
name: cm
`)
}

func TestSuffixTransformerWithDefaultFieldSpecs(t *testing.T) {
Copy link
Contributor

@yuwenma yuwenma Feb 9, 2022

Choose a reason for hiding this comment

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

Do you mind explaining how this test covers the code changes in plugin/builtin/suffixtransformer/SuffixTransformer.go?

IIUC, without the code in SuffixTransformer.go this test still passes, is that right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It differs from the previous test by the absence of the fieldSpecs attribute. As per the parent commit, the absence of fieldSpecs would otherwise result in an error

	if p.FieldSpecs == nil {
		return errors.New("fieldSpecs is not expected to be nil")
	}

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks!

}
if p.FieldSpecs == nil {
p.FieldSpecs = annotations.DefaultFieldSpecs()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with @KnVerey suggestion in #4404 but I'm wondering if we should change the Config() behavior this way.

c is a passed-in parameter which expects users to provide custom fieldspec string value. The useful return value would be either telling the unmarshalling error if the c is bad or found/not-found the fieldspec. Returning DefaultFieldSpecs() hides the message whether the json attribute "annotation" ever shows up or not in the passed-in string.

Considering the situations where
c = "annotation-typo:" and c="irrelevant: ", both will returns a smooth default fieldspec with "annotations: " meaning no annotation fieldspec specified. I don't think it's good for Config to swallow this info. @KnVerey @natasha41575 WDYT?

Copy link
Contributor

@natasha41575 natasha41575 Feb 9, 2022

Choose a reason for hiding this comment

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

I agree that we shouldn't swallow this error here; there are many reasons besides an empty c that p.FieldSpecs may be nil here.

Maybe an equivalent alternative that doesn't swallow errors for malformed values of c would be to check at the start of the function if c does not contain the string fieldSpecs, and in that case use the default fieldspecs?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe I'm being daft, but doesn't returning the error on unmarshal failure address that concern (i.e. the error is not swallowed, rendering the p.FieldSpecs clause unreachable in the event of failure). It's entirely likely that I'm misunderstanding, so please bear with me.

	if err = yaml.Unmarshal(c, p); err != nil {
		return err
	}

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I think you're right, sorry I missed that. @yuwenma I don't think we are swallowing anything that wasn't previously swallowed.

If there is a typo in c, e.g. annotations-typo: the transformer currently does nothing, silently.
This PR will change it so that if there is a typo in c, the transformer will have the default fieldspecs.

Neither is great, but I don't think this PR introduces a problem that wasn't already there, and I'm not sure how we would change it so I think this is fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe yaml.UnmarshalStrict disallows unknown fields, but perhaps that decision might be outside the scope of this particular behavioral change.

Copy link
Contributor

@yuwenma yuwenma Feb 10, 2022

Choose a reason for hiding this comment

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

The two examples are not for unmarshal error. Sorry I was distracted today. My main concern is the shifting behavior of transformers. Calling Config with passed-in c reads as "parsing the content of c and finding out the annotations pieces". So the valid return is either found c's content or nothing found (nil or error). If the p.FieldSpecs is reset to something not from c like
[fieldspec{path: "metadata/annotations"}, fieldspec{path: "spec/template/metadata/annotations"}, fieldspec{path: "spec/template/metadata/annotations", kind: "Deployment"}], it would be unexpected for users.
This is the swallowing info (or shifting behavior) I meant.

Copy link
Contributor Author

@khrisrichardson khrisrichardson Feb 10, 2022

Choose a reason for hiding this comment

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

I see. So your concern cuts to the core of the UX change and whether it's acceptable for the behavior to morph without some signal from the end-user about whether or not they accept the change in behavior. I share those concerns and wavered over whether the right answer was to introduce an attribute (e.g. - defaults: true), to version the builtin apiVersion (e.g. - apiVersion: builtin/v1alpha2), or to postpone until kustomize v5.

While you'd likely agree that it's not unheard of for defaults to be set in the absence of a value, something that may be lacking in this case is a manner for the end-user to probe what those defaults are via the CLI or glancing at API docs (another signal that suggests that perhaps builtin should be versioned, since those API docs would unlikely remain static). Right now it seems that the docs point the end-user to the source code to understand the shape of fieldSpecs but there doesn't appear to be any mention of what the defaults are when working with the kustomization field as opposed to a transformer.

Which brings up another point: the current behavior of transformers diverges from that of the kustomization field counterparts. The kustomization field uses the defaults, yet I believe it's possible to change the behavior either via a grandfathered configurations clause or by adding fieldSpecs inline. Neither of which get a mention in the kustomize docs but if you squint you can see the usage of configurations described here and the usage of inline fieldSpecs described here. So in a way, the change would be bringing alignment to divergent behavior between kustomize fields and transformers. I know that doesn't make any of this any easier to swallow.

I am fully appreciative of the fact that you are much better versed than I am when it comes to navigating best practices here, so am open to any input you have to improve the overall end-user experience.

Copy link
Contributor

Choose a reason for hiding this comment

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

@natasha41575 and I chatted about this today, and we're leaning towards wanting the new behaviour to be opt-in after all. Although we still agree that it seems very unlikely that using any builtin transformer with a nil fieldspec would make sense / have a useful result, it's also impossible to verify that. Knowing how important result stability is to our end users, that leads us to want to create a user-facing option for this. The use of fieldspecs in the configuration of our built in transformers is a convention, and we could enhance that convention by augmenting all the transformers that currently support it with an additional related field.

// Add the given annotations to the given field specifications.
type AnnotationsTransformerPlugin struct {
	Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
	FieldSpecs  []types.FieldSpec `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"`
	FieldSpecStrategy FieldSpecStrategy `json:"fieldSpecStrategy" yaml:"fieldSpecStrategy"`
}
apiVersion: builtin
kind: AnnotationsTransformer
metadata:
  name: not-important-to-example
annotations:
  app: myApp
  greeting/morning: a string with blanks
fieldSpecStrategy: merge | replace # defaults to replace for backwards compatibility
fieldSpecs:
- path: metadata/annotations
  create: true

With this solution:

  • We immediately have a way to use the defaults as-is like you wanted, albeit requiring an extra LOC.
  • We also immediately have a way to augment the defaults too.
  • In Kustomization v1, we could start versioning the built-ins as you suggest. In the new version of these transformers, we could flip the default for this new field to "merge" (although who knows... by then, maybe the openapi-based approach will be ready instead).

WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I appreciate the additional feedback and like the opt-in nature of your and @natasha41575's suggestion plus the stability it provides. And thank you @yuwenma for initiating this discussion. All the permutations make sense to me and I should be able to find some time to work on this tomorrow.

The one gap that I can conceive of with the merge behavior is perhaps the desire not to have it be exclusively additive, but instead out of the 10s of commonLabels field specs, perhaps one wishes to delete one or two via $patch: delete strategic merge semantics. Let me see what it will take to support that. I'm pretty sure examples abound within other parts of kustomize.

Copy link
Contributor

Choose a reason for hiding this comment

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

The idea that the merge could work as a real SMP and support deletion is interesting for sure! Feel free to investigate it in a follow up though.

@@ -48,3 +49,7 @@ func (f Filter) evaluateField(node *yaml.RNode) error {
return f.trackableSetter.SetScalar(fmt.Sprintf(
"%s%s", node.YNode().Value, f.Suffix))(node)
}

func DefaultFieldSpecs() types.FsSlice {
Copy link
Contributor

@yuwenma yuwenma Feb 9, 2022

Choose a reason for hiding this comment

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

builtinconfig.MakeDefaultConfig().NameSuffix seems to be handy enough to be called directly. Maybe not need a new function.

Copy link
Contributor Author

@khrisrichardson khrisrichardson Feb 9, 2022

Choose a reason for hiding this comment

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

I tried that first, but given that builtinconfig is internal to api, it was not accessible from plugin/buitin

Copy link
Contributor

@yuwenma yuwenma Feb 9, 2022

Choose a reason for hiding this comment

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

oh, I see! Yeah...

Copy link
Contributor

Choose a reason for hiding this comment

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

@natasha41575 Shall we surface the builtin const fieldspec var so users can import directly?

Copy link
Contributor

@natasha41575 natasha41575 Feb 22, 2022

Choose a reason for hiding this comment

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

The default fieldspecs are already surfaced here:

func GetDefaultFieldSpecsAsMap() map[string]string {

Rather than surfacing the default fieldspecs in each filter individually as done in this PR, I would suggest adding a new method to this file, maybe something like func GetDefaultFieldSpecFor(filter string) string, or just use the existing method GetDefaultFieldSpecsAsMap() to get the default fieldspecs. A call for these methods might look like GetDefaultFieldSpecFor("nameprefix") or GetDefaultFieldSpecsAsMap()["nameprefix"].

Since it's already exposed, I don't think we need to create a new DefaultFieldSpecs() in each filter.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 to call GetDefaultFieldSpecsAsMap()

@yuwenma
Copy link
Contributor

yuwenma commented Feb 10, 2022

/hold cancel

@k8s-ci-robot k8s-ci-robot removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Feb 10, 2022
@k8s-ci-robot k8s-ci-robot added size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. and removed size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Mar 17, 2022
@khrisrichardson khrisrichardson changed the title use default fieldSpecs in transformer configs if nil (#4404) merge/replace fieldSpecs in transformer configs per strategy (#4404) Mar 19, 2022
@natasha41575
Copy link
Contributor

@KnVerey

@k8s-ci-robot
Copy link
Contributor

@khrisrichardson: PR needs rebase.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository.

@k8s-ci-robot k8s-ci-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 15, 2022
@k8s-triage-robot
Copy link

The Kubernetes project currently lacks enough contributors to adequately respond to all issues and PRs.

This bot triages issues and PRs according to the following rules:

  • After 90d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was applied, the issue is closed

You can:

  • Mark this issue or PR as fresh with /remove-lifecycle stale
  • Mark this issue or PR as rotten with /lifecycle rotten
  • Close this issue or PR with /close
  • Offer to help out with Issue Triage

Please send feedback to sig-contributor-experience at kubernetes/community.

/lifecycle stale

@k8s-ci-robot k8s-ci-robot added the lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale. label Oct 13, 2022
@k8s-triage-robot
Copy link

The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.

This bot triages issues and PRs according to the following rules:

  • After 90d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was applied, the issue is closed

You can:

  • Mark this issue or PR as fresh with /remove-lifecycle rotten
  • Close this issue or PR with /close
  • Offer to help out with Issue Triage

Please send feedback to sig-contributor-experience at kubernetes/community.

/lifecycle rotten

@k8s-ci-robot k8s-ci-robot added lifecycle/rotten Denotes an issue or PR that has aged beyond stale and will be auto-closed. and removed lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale. labels Nov 12, 2022
@k8s-triage-robot
Copy link

The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.

This bot triages PRs according to the following rules:

  • After 90d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was applied, the PR is closed

You can:

  • Reopen this PR with /reopen
  • Mark this PR as fresh with /remove-lifecycle rotten
  • Offer to help out with Issue Triage

Please send feedback to sig-contributor-experience at kubernetes/community.

/close

@k8s-ci-robot
Copy link
Contributor

@k8s-triage-robot: Closed this PR.

In response to this:

The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.

This bot triages PRs according to the following rules:

  • After 90d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was applied, the PR is closed

You can:

  • Reopen this PR with /reopen
  • Mark this PR as fresh with /remove-lifecycle rotten
  • Offer to help out with Issue Triage

Please send feedback to sig-contributor-experience at kubernetes/community.

/close

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. lifecycle/rotten Denotes an issue or PR that has aged beyond stale and will be auto-closed. needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. ok-to-test Indicates a non-member PR verified by an org member that is safe to test. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants