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

Add transformer to apply json patch6902 #300

Merged
merged 3 commits into from
Aug 31, 2018

Conversation

Liujingfang1
Copy link
Contributor

@Liujingfang1 Liujingfang1 commented Aug 28, 2018

break up #298

To avoid import cycle, I add the transformer files to pkg/patch/transformer

@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: Liujingfang1

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

The pull request process is described 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

@k8s-ci-robot k8s-ci-robot added approved Indicates a PR has been approved by an approver from all required OWNERS files. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. labels Aug 28, 2018
@Liujingfang1 Liujingfang1 requested review from mengqiy and removed request for grodrigues3 August 28, 2018 22:39
Copy link
Contributor

@monopole monopole left a comment

Choose a reason for hiding this comment

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

@mengqiy ? WDYT?

)

// NewPatchJson6902 load a slice of PatchJson6902
func NewPatchJson6902(l loader.Loader, slice []PatchJson6902) (map[resource.ResId][]byte, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

LoadPatchJson6902Map

It doesn't make instances of PatchJson6902, and calling it Load a signals the need for a loader.

"",
p.Target.Namespace,
)
content, err := l.Load(p.Path)
Copy link
Contributor

@monopole monopole Aug 29, 2018

Choose a reason for hiding this comment

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

This approach is consistent with what we do for SMP, but seeing it now this way (and noticing this function has no test) makes me wonder if we'd be better off specifying the patch string directly in the kustomization file.

We didn't do this for SMP because those patches look like resources, and specifying a resource as a string field in another resource would be odd. Plus SMP's combine the path to the thing they are modifying with the thing they modify - they are different.

Seems like

patchesJson6902:
- target:   blah 
   - op: replace
     path: /spec/template/spec/containers/0/name
     value: my-nginx
   - op: add
     path: /spec/replica
     value: 3

would be easier to maintain in a kustomization file than a distinct JSON patch file.

perhaps it boils down to a question of who writes the JSON patch - human or tool?

The separate file approach is better for the tool, this inlining seems better for the human.

Of course we'd convert these fields to standard JSON {"op": "replace", "path": "/spec/template/spec/containers/0/name", "value": "my-nginx"}, before feeding them into the library call.

If not, we need a regression test using a fake file loader for this Load function

Copy link
Contributor

Choose a reason for hiding this comment

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

@mxey 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.

This format is more straightforward to me.
From what I read from issues, human is more likely to write JSON patch, especially for changing some port, dns names.

Copy link
Contributor

Choose a reason for hiding this comment

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

sgtm, lets make it in line .

yay, no loader!

Copy link
Contributor

Choose a reason for hiding this comment

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

@monopole I like the inline approach in general, but I can see a need for a tool writing patches, like setting Ingress hostnames for feature branches. If it's inline, there should also be a command to add those.

Copy link
Member

Choose a reason for hiding this comment

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

I have some concerns:
Make json patch inline means that kustomize need to understand json patches e.g. how to decoding it. There are more fields than what @Liujingfang1 already wrote, e.g. from.
IMO making json patch inline pushes some complexity of json patch to the kustomize layer. Ideally, kustomize doesn't need to understand it. It should simply hand the byte array to the library.

// patchJson6902Transformer applies patches.
type patchJson6902Transformer struct {
patches map[resource.ResId][]byte
}
Copy link
Contributor

Choose a reason for hiding this comment

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

How about just

type patchJson6902Map map[resource.ResId][]byte

and use this to hang transformer methods from. We return a transformer.Transformer anyway...

Copy link
Contributor

Choose a reason for hiding this comment

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

So this changes to

map[resource.ResId][]WhateverWeCallTheOpPathValueTuple

:)

@Liujingfang1
Copy link
Contributor Author

Changed the format of patchesJson6902. It doesn't rely on loader now. The patch operations can be specified directly in kustomization.yaml.

@Liujingfang1
Copy link
Contributor Author

If there are several or many operations, the kustomization.yaml will contain a large block of them. Here is an example of this format

resources:
- the-map.yaml
- another-map.yaml

patchesJson6902:
- target:
    kind: ConfigMap
    name: the-map
  jsonPatch:
  - op: add
    path: /data/username
    value: any

- target:
    kind: ConfigMap
    name: another-map
  jsonPatch:
  - op: add
    path: /data/username
    value: xiaoming
  - op: add
    path: /data/password
    value: mima
  - op: replace
    path: /data/altGreeting
    value: "Good Evening!"

@Liujingfang1
Copy link
Contributor Author

@monopole After coding on this format for a few hours, I realized it increases the complexity beyond my expectation. The JSON patch need to support 5 different operations. Some has path field, some has from field.

 [
     { "op": "test", "path": "/a/b/c", "value": "foo" },
     { "op": "remove", "path": "/a/b/c" },
     { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
     { "op": "replace", "path": "/a/b/c", "value": 42 },
     { "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
     { "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
   ]

If we load a JSON patch from a file, we get []byte and feed it to json-patch library.

With this new format, we need to add logic validating the operations and path or from match.
We also need to construct a []byte for the JSON patch after loading a kustomization.yaml. Constructing this []byte by ourselves is error-prone. In my code, I use string for value, but the content in this filed can be int, string, slice depending on the type of the corresponding field. I think we'd better avoid handling that complexity in Kustomize.

@monopole
Copy link
Contributor

monopole commented Aug 29, 2018

A subset of 6902 - just add, delete and replace as described above might be enough (and would make the kustomization easier to read/maintain). From a coding point of view, we could silently attempt a conversion of anything that's not explicitly quoted to a number or slice, and if it worked take the appropriate action when creating the json to feed to evanphx/json-patch. Or perhaps skip that library and the json conversion entirely, and just perform the transformation directly, given op, path and value. Assuming the common use case here is swapping a hostname or tag, changing a replica count, etc., not something super complex.

But we have no data besides @mxey's comment as to how people want to do this.

So OK with doing the simpler coding problem of reading the file.

Can we keep it forward compatible with the less pedantic approach proposed above?

E.g. a structure with a target and a path, and possibly later a slice of operations (user would be required to omit the path field if they wanted to specify inline operations). That might be the best of both worlds.

@Liujingfang1
Copy link
Contributor Author

@monopole @mengqiy I found one library https://github.com/krishicks/yaml-patch which can handle YAML format for JSON patch. Seems we can use it and I will test it. If it works, we can keep the inline format without handling the complexity by ourselves.

@monopole
Copy link
Contributor

monopole commented Aug 30, 2018

sgtm! What we're going for here is a means to not have to write new kustomization directives for every small edit (e.g. changing the image name).

But I'm still in favor of doing both (reading in a complete JSON6902 patch file or accepting inline directives).

The copy aspect of 6902 could be a means to reduce pod template duplication.

@mxey
Copy link
Contributor

mxey commented Aug 30, 2018

I think Kustomize should support the entire JSON Patch spec, especially if it references the RFC in the property name. I personally have a potential use case for copy: turning one ingress rule into multiple ones for different hostnames.

Also, it would be consistent with what kubectl supports.

@k8s-ci-robot k8s-ci-robot added size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. and removed size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Aug 30, 2018
@Liujingfang1
Copy link
Contributor Author

Liujingfang1 commented Aug 30, 2018

  • Changed the struct of PatchJSON6902. It can handle both a relative path and inline operations.
  • The inline operations are in YAML format, the patching is handled by functions from https://github.com/krishicks/yaml-patch, which has a complete support for JSON patch in YAML format
  • The file from a relative path is currently assumed to be JSON format. The patching is handled by functions from https://github.com/evanphx/json-patch
  • The file can also be YAML format. We can add this support later.

@monopole @mengqiy @mxey PTAL

Copy link
Contributor

@monopole monopole left a comment

Choose a reason for hiding this comment

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

Looks great, a few nits.

t.target.Name,
"",
t.target.Namespace,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

If this is done in the factory, then both transformation types can have a ResId field instead of patch.Target, and we'd have only one copy of this code instead of two.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This sounds good. Will update it.

decodedPatch, err := jsonpatch.DecodePatch(t.operations)
if 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.

Also, this decode could move to the factory, so this error would happen earlier.

s/operations []byte/patch jsonpatch.Patch/ in patchJson6902JSONTransformer

then patchJson6902JSONTransformer would look more like patchJson6902YAMLTransformer

"",
t.target.Namespace,
)

Copy link
Contributor

Choose a reason for hiding this comment

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

so this would go away

return fmt.Errorf("found multiple objects that the patch can apply %v", matchedIds)
}

obj := baseResourceMap[matchedIds[0]]
Copy link
Contributor

Choose a reason for hiding this comment

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

should only have one copy of L60-78.

shared package private helper func

}

return transformers.NewNoOpTransformer(), nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

could call this file factory.go

please add a test with a fake loader. Nothing too complex, but enough to capture the error conditions. i.e. don't actually look at the transformers returned

@Liujingfang1
Copy link
Contributor Author

  • address comments.
  • add factory.go and tests.

Copy link
Contributor

@monopole monopole left a comment

Choose a reason for hiding this comment

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

/lgtm

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Aug 31, 2018
@k8s-ci-robot k8s-ci-robot merged commit babfb3f into kubernetes-sigs:master Aug 31, 2018
@mengqiy
Copy link
Member

mengqiy commented Aug 31, 2018

I'm glad that now we have a solution that can handle both inline and from-file json patch!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. lgtm "Looks good to me", indicates that a PR is ready to be merged. 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.

None yet

5 participants