Compare commits

..

61 Commits

Author SHA1 Message Date
Kubernetes Prow Robot
c464fb0a81 Merge pull request #1436 from richardmarshall/kubectl_clarity
docs: Additional details for kubectl integration
2019-08-13 15:18:23 -07:00
Kubernetes Prow Robot
694cf23df8 Merge pull request #1432 from richardmarshall/lostreplicas
Retain replicas field in edit marshal path
2019-08-09 14:29:14 -07:00
Richard Marshall
e66656aa7f docs: Additional details for kubectl integration 2019-08-08 17:06:19 -07:00
Richard Marshall
eaae7af5fe Retain replicas field in edit marshal path 2019-08-08 15:45:56 -07:00
Kubernetes Prow Robot
f9fe138114 Merge pull request #1416 from anthonyho007/makefile
add Makefile for local development
2019-08-07 11:04:09 -07:00
Kubernetes Prow Robot
2a2a889c37 Merge pull request #1423 from sunny0826/master
Update zh-README.md & zh-example-README.md
2019-08-02 12:37:55 -07:00
郭旭东
34287e511f fix example-zh-README.md 2019-08-02 09:09:32 +08:00
Anthony Ho
e6fffc8ba4 add makefile 2019-08-01 11:23:38 -04:00
郭旭东
86f221611e Update zh-example-README.md 2019-08-01 15:30:11 +08:00
郭旭东
b4d6e89fa2 Update zh-README.md 2019-08-01 15:19:14 +08:00
Kubernetes Prow Robot
5937bd0259 Merge pull request #1394 from richardmarshall/namerefperformance
Simplify name reference candidate resmap building
2019-07-31 10:18:14 -07:00
Richard Marshall
ed3c29be12 Simplify name reference candidate resmap building
This patch removes a layer of looping in the name reference candiate
resmap building process by not checking if the resources already exist
in the new resmap.
2019-07-30 17:15:15 -07:00
Kubernetes Prow Robot
3d2e956b19 Merge pull request #1412 from richardmarshall/anchor_resmap_select
Automatically anchor resource selector patterns
2019-07-30 15:47:11 -07:00
Kubernetes Prow Robot
dd9d1f95e9 Merge pull request #1389 from Liujingfang1/repospec
make repospec memebers public
2019-07-30 15:27:51 -07:00
jingfangliu
a279c08f7d make repospec memebers public 2019-07-30 13:56:20 -07:00
Kubernetes Prow Robot
a798109161 Merge pull request #1413 from bai/fix-typo
Fix typo in patches definition
2019-07-30 10:14:51 -07:00
Vlad Gorodetsky
bafd6b5423 Fix typo in patches definition 2019-07-30 14:52:02 +03:00
Richard Marshall
963913f9ef Automatically anchor resource selector patterns 2019-07-29 17:57:33 -07:00
Jingfang Liu
46905588ac add document for inline patch (#1411) 2019-07-29 15:15:06 -07:00
Kubernetes Prow Robot
5426888df4 Merge pull request #1405 from Liujingfang1/inlinepatch
add inline patch support for Strategic Merge Patch and JSON patch
2019-07-29 14:28:49 -07:00
jingfangliu
35481ec6d9 add inline patch support for Strategic Merge Patch and JSON patch 2019-07-29 14:10:57 -07:00
Kubernetes Prow Robot
6c92c30e94 Merge pull request #1402 from damienr74/currentid-replicas
Allow replicas to find modified names.
2019-07-29 12:54:47 -07:00
Damien Robichaud
02f6b3ec98 Allow replicas to find modified names.
Also allows to test for modified resmaps instead of directly loading
them.
2019-07-26 18:00:59 -07:00
Kubernetes Prow Robot
a9848f2738 Merge pull request #1403 from Liujingfang1/inlinepatch
add testing for patch transformers
2019-07-26 15:05:57 -07:00
jingfangliu
b4038a6cd2 add testting for patch transformers 2019-07-26 14:02:52 -07:00
Kubernetes Prow Robot
95f3303493 Merge pull request #1400 from keleustes/defaultsa
Force the namespace value for the "default" service object.
2019-07-26 10:51:21 -07:00
Jerome Brette
2faf4a491b Force the namespace value for the "default" service object.
The clusterrolebinding and rolebinding is pointing at a resource
which is not listed in the kustomize
2019-07-25 22:43:59 +00:00
Kubernetes Prow Robot
e646bba1ff Merge pull request #1396 from Liujingfang1/delete
support strategic merge patch with $patch: delete
2019-07-25 12:24:45 -07:00
Kubernetes Prow Robot
99a21b0a3c Merge pull request #1308 from keleustes/varequal
Demonstrate need for Var.DeepEqual method equivalent
2019-07-25 11:26:06 -07:00
Kubernetes Prow Robot
e7a22b6bc5 Merge pull request #1398 from keleustes/doc
Update v3.1.0 release notes.
2019-07-25 10:43:28 -07:00
Jerome Brette
d783bbc0bc DeepEqual method seems cleaner than adding Defaulting before every
reflect.DeepEqual call
2019-07-25 03:52:25 +00:00
Jerome Brette
b7405f3872 Test new types.Var.DeepEqual method. 2019-07-25 03:50:28 +00:00
Jerome Brette
abc419b5f9 Add Absorb method to VarSet and DeepEqual to Var 2019-07-25 03:42:40 +00:00
Jerome Brette
336378b114 Update release notes 2019-07-25 00:12:51 +00:00
jingfangliu
29959551da release note for v3.1.0 2019-07-24 14:16:09 -07:00
jingfangliu
fc78917191 support strategic merge patch with $patch: delete 2019-07-24 12:46:33 -07:00
Kubernetes Prow Robot
ffd95ef5a9 Merge pull request #1378 from keleustes/nameprefix
Name Prefix and Suffix Transformation for multi level level kustomize context
2019-07-24 11:33:21 -07:00
Jerome Brette
230090d790 Fix namereference and stacked kustomization contexts (3/3)
- Update unit and integration tests.
2019-07-24 18:02:29 +00:00
Jerome Brette
8fa3861ba3 Fix namereference and stacked kustomization contexts (2/3)
- Leverage nameprefix and namesuffix contextual data
2019-07-24 18:02:23 +00:00
Jerome Brette
69c90e3427 Fix namereference and stacked kustomization contexts (1/3)
- Update PrefixSuffixTransfomer to add empty prefix and suffix
2019-07-24 10:59:07 -05:00
Kubernetes Prow Robot
5a73f345fd Merge pull request #1388 from Liujingfang1/patch
add example for extended patches
2019-07-23 17:04:16 -07:00
jingfangliu
0e62d759f0 address comments 2019-07-23 16:55:58 -07:00
jingfangliu
b2967d2f77 add example for extended patches 2019-07-23 15:54:25 -07:00
Kubernetes Prow Robot
c23039c07a Merge pull request #1379 from keleustes/namespace
Update Namespace and Name simultaneously
2019-07-23 14:10:14 -07:00
Kubernetes Prow Robot
5747c417c4 Merge pull request #1363 from Liujingfang1/patch
update edit fix to convert the old patches to patchesStrategicMerge
2019-07-23 13:16:14 -07:00
jingfangliu
8c53d77111 update edit fix to convert the old patches to patchesStrategicMerge 2019-07-23 10:38:48 -07:00
Jerome Brette
01667cabde Update Namespace and Name simultaneously (2/2)
Add tests combining prefixsuffix and namespace transformers.
2019-07-23 11:04:59 -05:00
Jerome Brette
f649b62629 Update Namespace and Name simultaneously (1/2)
- Removed RoleBinding and Webhook specific code in the namespacetransformer.
  That code was attempting to perform the task of the namereference
- Updated namereference transformer configuration to suppport the
  Webhooks.
- Prevent the namereference from wiping out the namespace value if
  no referral candidate was selected
- Added unit tests.
2019-07-23 11:04:52 -05:00
Kubernetes Prow Robot
3a4d025b5c Merge pull request #1371 from keleustes/nsvar
Add namespace to variable definition
2019-07-22 11:04:53 -07:00
Jerome Brette
99eb08eb1e Add Namespace to var definition to allow disambiguation 2019-07-19 12:08:38 -05:00
Kubernetes Prow Robot
d3f8c0d87f Merge pull request #1327 from keleustes/residrequals-variables
ResId.Equals usable for VariableRef.
2019-07-19 09:41:12 -07:00
Jerome Brette
0bec7b996b Code review implementation for namespace needed in vars 2019-07-18 19:20:10 -05:00
Jerome Brette
dd5674fe6b ResId.Equals usable for VariableRef.
- Namespace need objRef field in variable declaration
- Add namespace conflict test for variables

The replacement of ResId.GkvnEquals reference by ResId.Equals
highligthed the fact it is no possible yet when looking for
variable targets because the namespace field is not allowed yet.
This commit adds two tests to the namespaces_test.go regarding
that use case.
2019-07-18 19:20:10 -05:00
Kubernetes Prow Robot
33159c26df Merge pull request #1369 from Liujingfang1/order
add ResourceQuota to the order list
2019-07-18 16:35:51 -07:00
Kubernetes Prow Robot
afc7dbebe5 Merge pull request #1326 from keleustes/residequals-patchtransformer
Residequals patchtransformer
2019-07-18 13:14:19 -07:00
Jerome Brette
f363acf839 Implement code review changes for ResId.Equals instead of ResId.GkvnEquals 2019-07-18 14:13:51 -05:00
Kubernetes Prow Robot
96d5a7401d Merge pull request #1365 from richardmarshall/fix_integration_test
Fix kustomize install in integration test
2019-07-18 11:56:26 -07:00
jingfangliu
403fa20546 add ResourceQuota and LimitRange to the order list 2019-07-18 11:44:46 -07:00
Richard Marshall
ba4d7ddca8 test: Fix kustomize install in integration test 2019-07-17 20:28:47 -07:00
Jerome Brette
5116e2f210 Improve Transformer with Namespace tests.
- Reorganize test into test tables.
- Ensure that every test case, convers SMP and JSONPatch by
  using Deployment as kind first and then "MyCRD" as kind.
- Add tests involving namespaces.
- Add tests involving reordering of patches.
2019-07-17 15:44:44 -05:00
Jerome Brette
9e0f198227 Start to phase out usage ResId.GvknEquals where possible 2019-07-17 15:44:44 -05:00
54 changed files with 3109 additions and 381 deletions

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@
*.dll
*.so
*.dylib
/kustomize
# Test binary, build with `go test -c`
*.test

28
Makefile Normal file
View File

@@ -0,0 +1,28 @@
BIN_NAME=kustomize
export GO111MODULE=on
all: test build
test: generate-code test-lint test-go
test-go:
go test -v ./...
test-lint:
golangci-lint run ./...
generate-code:
./plugin/generateBuiltins.sh $(GOPATH)
build:
go build -o $(BIN_NAME) cmd/kustomize/main.go
install:
go install $(PWD)/cmd/kustomize
clean:
go clean
rm -f $(BIN_NAME)
.PHONY: test build install clean generate-code test-go test-lint

View File

@@ -24,8 +24,16 @@ these [instructions](docs/INSTALL.md).
Browse the [docs](docs) or jump right into the
tested [examples](examples).
kustomize [v2.0.3] is available in [kubectl v1.14 and v1.15][kubectl].
## kubectl integration
Since [v1.14][kubectl announcement] the kustomize build system has been included in kubectl.
| kubectl version | kustomize version |
|---------|--------|
| v1.15.x | [v2.0.3](https://github.com/kubernetes-sigs/kustomize/tree/v2.0.3) |
| v1.14.x | [v2.0.3](https://github.com/kubernetes-sigs/kustomize/tree/v2.0.3) |
For examples and guides for using the kubectl integration please see the [kubectl book] or the [kubernetes documentation].
## Usage
@@ -161,7 +169,9 @@ is governed by the [Kubernetes Code of Conduct].
[imageBase]: docs/images/base.jpg
[imageOverlay]: docs/images/overlay.jpg
[kind/feature]: https://github.com/kubernetes-sigs/kustomize/labels/kind%2Ffeature
[kubectl]: https://kubernetes.io/blog/2019/03/25/kubernetes-1-14-release-announcement
[kubectl announcement]: https://kubernetes.io/blog/2019/03/25/kubernetes-1-14-release-announcement
[kubectl book]: https://kubectl.docs.kubernetes.io/pages/app_customization/introduction.html
[kubernetes documentation]: https://kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/
[kubernetes style]: docs/glossary.md#kubernetes-style-object
[kustomization]: docs/glossary.md#kustomization
[overlay]: docs/glossary.md#overlay

View File

@@ -23,7 +23,9 @@ English | [简体中文](zh/README.md)
## Release notes
* [3.0](v3.0.0.md) - Late June 2019. Plugin developer release.
* [3.1](v3.1.0.md) - Late July 2019. Extended patches and improved resource matching.
* [3.0](v3.0.0.md) - Late June 2019. Plugin developer release.
* [2.1](v2.1.0.md) - 18 June 2019. Plugins, ordered resources, etc.

View File

@@ -38,6 +38,7 @@ What transformations (customizations) should be applied?
| [namePrefix](#nameprefix) | string | Prepends value to the names of all resources |
| [nameSuffix](#namesuffix) | string | The value is appended to the names of all resources. |
| [replicas](#replicas) | list | Replicas modifies the number of replicas of a resource. |
| [patches](#patches) | list | Each entry should resolve to a patch that can be applied to multiple targets. |
|[patchesStrategicMerge](#patchesstrategicmerge)| list |Each entry in this list should resolve to a partial or complete resource definition file.|
|[patchesJson6902](#patchesjson6902)| list |Each entry in this list should resolve to a kubernetes object and a JSON patch that will be applied to the object.|
|[transformers](#transformers)|list|[plugin](plugins) configuration files|
@@ -263,11 +264,47 @@ resource type is ConfigMap or Secret.
nameSuffix: -v2
```
### patches
Each entry in this list should resolve to an Patch object,
which includes a patch and a target selector.
The patch can be either a strategic merge patch or a JSON patch.
it can be either a patch file or an inline string.
The target selects
resources by group, version, kind, name, namespace,
labelSelector and annotationSelector. A resource
which matches all the specified fields is selected
to apply the patch.
```
patches:
- path: patch.yaml
target:
group: apps
version: v1
kind: Deployment
name: deploy.*
labelSelector: "env=dev"
annotationSelector: "zone=west"
- patch: |-
- op: replace
path: some/existing/path
value: new value
target:
kind: MyKind
labelSelector: "env=dev"
```
The `name` and `namespace` fields of the patch target selector are
automatically anchored regular expressions. This means that the value `myapp`
is equivalent to `^myapp$`.
### patchesStrategicMerge
Each entry in this list should be a relative path
Each entry in this list should be either a relative
file path or an inline content
resolving to a partial or complete resource
definition file.
definition.
The names in these (possibly partial) resource
files must match names already loaded via the
@@ -286,6 +323,22 @@ patchesStrategicMerge:
- deployment_increase_memory.yaml
```
The patch content can be a inline string as well.
```
patchesStrategicMerge:
- |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
image: nignx:latest
```
### patchesJson6902
Each entry in this list should resolve to
@@ -330,6 +383,23 @@ patchesJson6902:
path: add_service_annotation.yaml
```
The patch content can be an inline string as well:
```
patchesJson6902:
- target:
version: v1
kind: Deployment
name: my-deployment
patch: |-
- op: add
path: /some/new/path
value: value
- op: replace
path: /some/existing/path
value: "new value"
```
### replicas
Replicas modified the number of replicas for a resource.

127
docs/v3.1.0.md Normal file
View File

@@ -0,0 +1,127 @@
# kustomize 3.1.0
## Extended patches
Since this version, Kustomize allows applying one patch to multiple resources. This works for both Strategic Merge Patch and JSON Patch. Take a look at [patch multiple objects](../examples/patchMultipleObjects.md).
## Improved Resource Matching
Multiple improvements have been made to allow the user to leverage "namespace"
instead/or with "name suffix/prefix" to segregate resources.
### Patch resolution improvement
The following example demonstrates how using the namespace field in the patch definition,
will let the user define two different patches against two different Deployment having the
same "deploy1" name but in different namespaces in the same Kustomize context/folder.
Unless the `namespace:` field has been specified in the kustomization.yaml, no namespace
value will be handled as Kubernetes `default` namespace.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy1
namespace: main
spec:
template:
spec:
containers:
- name: nginx
env:
- name: ANOTHERENV
value: TESTVALUE
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy1
namespace: production
spec:
template:
spec:
containers:
- name: main
env:
- name: ANOTHERENV
value: PRODVALUE
```
### Variable resolution improvement
It is possible to add namespace field to the variable declaration. In the following example,
two `Service` objects with the same `elasticsearch` name have been declared.
Specifying the namespace in the objRef of the corresponding varriables, allows Kustomize to
resovlve thoses variables.
If the namespace is not specified, Kustomize will handle it has a "wildcard" value.
Extract of kustomization.yaml:
```yaml
vars:
- name: elasticsearch-test-protocol
objref:
kind: Service
name: elasticsearch
namespace: test
apiVersion: v1
fieldref:
fieldpath: spec.ports[0].protocol
- name: elasticsearch-dev-protocol
objref:
kind: Service
name: elasticsearch
namespace: dev
apiVersion: v1
fieldref:
fieldpath: spec.ports[0].protocol
```
### Simultaneous change of names and namespaces
Kustomize is now able to deal with simultaneous changes of name and namespace.
Special attention has been paid the handling of:
- ClusterRoleBinding/RoleBinding "subjects" field,
- ValidatingWebhookConfiguration "webhooks" field.
The user should be able to use a kustomization.yaml as shown in the example bellow
even if ClusterRoleBind,RoleBinding and ValidatingWebookConfiguration are part of the
resources he needs to declare.
Extract of kustomization.yaml:
```yaml
namePrefix: pfx-
nameSuffix: -sfx
namespace: testnamespace
resources:
...
```
### Resource and Kustomize Context matching.
Kustomize is now able to support more aggregation patterns.
If for instance, the top level of kustomization.yaml, is simply
combining sub-components, (as in the following example), Kustomize has improved
resource matching capabilities. This removes some of the constraints which were
present on the utilization of prefix/suffix and namespace transformers in the
individual components.
```yaml
resources:
- ../component1
- ../component2
- ../component3
```
## Other improvements
- Image transformation has been improved. This allows the user to update the sha256 of
an image with another sha256.
- Multiple default transformer configuration entries have been added, removing the need for the
user to add them as part of the `configurations:` section of the kustomization.yaml.
- `kustomize` help command has been tidied up.

View File

@@ -19,6 +19,8 @@
## 发行说明
* [3.1](../v3.1.0.md) - 2019年7月下旬扩展 patches 和改进的资源匹配。
* [3.0](../v3.0.0.md) - 2019年6月下旬插件开发者发布。
* [2.1](../v2.1.0.md) - 2019年6月18日

View File

@@ -33,6 +33,8 @@ Basic Usage
* [json patch](jsonpatch.md) - Apply a json patch in a kustomization
* [patch multiple objects](patchMultipleObjects.md) - Apply a patch to multiple objects
Advanced Usage
- generator plugins:

View File

@@ -28,7 +28,7 @@
# before running it.
#
# At time of writing, its 'call point' was in
# https://github.com/kubernetes/test-infra/blob/master/jobs/config.json
# https://github.com/kubernetes/test-infra/blob/master/config/jobs/kubernetes-sigs/kustomize/kustomize-config.yaml
function exitWith {
local msg=$1
@@ -53,7 +53,7 @@ function setUpEnv {
exitWith "Script must be run from $expectedRepo"
fi
GO111MODULE=on go install . || \
GO111MODULE=on go install ./cmd/kustomize || \
{ exitWith "Failed to install kustomize."; }
PATH=$GOPATH/bin:$PATH

View File

@@ -0,0 +1,188 @@
[Strategic Merge Patch]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-api-machinery/strategic-merge-patch.md
[JSON patches]: https://tools.ietf.org/html/rfc6902
[label selector]: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors
# Demo: applying a patch to multiple resources
A kustomization file supports customizing resources via both
[Strategic Merge Patch] and [JSON patches]. Now one patch can be
applied to multiple resources.
This can be done by specifying a patch and a target selector as follows:
```
patches:
- path: <PatchFile>
target:
group: <Group>
version: <Version>
kind: <Kind>
name: <Name>
namespace: <Namespace>
labelSelector: <LabelSelector>
annotationSelector: <AnnotationSelector>
```
Both `labelSelector` and `annotationSelector` should follow the convention in [label selector].
Kustomize selects the targets which match all the fields in `target` to apply the patch.
The example below shows how to inject a sidecar container for all deployment resources.
Make a `kustomization` containing a Deployment resource.
<!-- @createDeployment @test -->
```
DEMO_HOME=$(mktemp -d)
cat <<EOF >$DEMO_HOME/kustomization.yaml
resources:
- deployments.yaml
EOF
cat <<EOF >$DEMO_HOME/deployments.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy1
spec:
template:
metadata:
labels:
old-label: old-value
spec:
containers:
- name: nginx
image: nginx
args:
- one
- two
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy2
spec:
template:
metadata:
labels:
key: value
spec:
containers:
- name: busybox
image: busybox
EOF
```
Declare a Strategic Merge Patch file to inject a sidecar container:
<!-- @addPatch @test -->
```
cat <<EOF >$DEMO_HOME/patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: not-important
spec:
template:
spec:
containers:
- name: istio-proxy
image: docker.io/istio/proxyv2
args:
- proxy
- sidecar
EOF
```
Apply the patch by adding _patches_ field in kustomization.yaml
<!-- @applyPatch @test -->
```
cat <<EOF >>$DEMO_HOME/kustomization.yaml
patches:
- path: patch.yaml
target:
kind: Deployment
EOF
```
Running `kustomize build $DEMO_HOME`, in the output confirm that both Deployment resources are patched correctly.
<!-- @confirmPatch @test -->
```
test 2 == \
$(kustomize build $DEMO_HOME | grep "image: docker.io/istio/proxyv2" | wc -l); \
echo $?
```
The output is as follows:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy1
spec:
template:
metadata:
labels:
old-label: old-value
spec:
containers:
- args:
- proxy
- sidecar
image: docker.io/istio/proxyv2
name: istio-proxy
- args:
- one
- two
image: nginx
name: nginx
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy2
spec:
template:
metadata:
labels:
key: value
spec:
containers:
- args:
- proxy
- sidecar
image: docker.io/istio/proxyv2
name: istio-proxy
- image: busybox
name: busybox
```
## Target selector
- Select resources with name matching `name*`
```yaml
target:
name: name*
```
- Select all Deployment resources
```yaml
target:
kind: Deployment
```
- Select resources matching label `app=hello`
```yaml
target:
labelSelector: app=hello
```
- Select resources matching annotation `app=hello`
```yaml
target:
annotationSelector: app=hello
```
- Select all Deployment resources matching label `app=hello`
```yaml
target:
kind: Deployment
labelSelector: app=hello
```

View File

@@ -26,6 +26,8 @@ go get sigs.k8s.io/kustomize/v3/cmd/kustomize
* [json patch](jsonpatch.md) -在 kustomization 中应用 json patch 。
* [patch multiple objects](patchMultipleObjects.md) - 通过一个patch来修改多个资源。
高级用法
- generator 插件:
@@ -34,6 +36,10 @@ go get sigs.k8s.io/kustomize/v3/cmd/kustomize
* [secret generation](../secretGeneratorPlugin.md) - 生成 Secret。
- transformer 插件:
* [validation transformer](../validationTransformer/README.md) - 通过 transformer 验证资源。
- 定制内建 transformer 配置
* [transformer configs](../transformerconfigs/README.md) - 自定义 transformer 配置。

1
go.sum
View File

@@ -149,7 +149,6 @@ k8s.io/klog v0.3.3 h1:niceAagH1tzskmaie/icWd7ci1wbG7Bf2c6YGcQv+3c=
k8s.io/klog v0.3.3/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/kube-openapi v0.0.0-20190603182131-db7b694dc208 h1:5sW+fEHvlJI3Ngolx30CmubFulwH28DhKjGf70Xmtco=
k8s.io/kube-openapi v0.0.0-20190603182131-db7b694dc208/go.mod h1:nfDlWeOsu3pUf4yWGL+ERqohP4YsZcBJXWMK+gkzOA4=
sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0=
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=

View File

@@ -338,7 +338,11 @@ func (fs *UnstructAdapter) Patch(patch ifc.Kunstructured) error {
}
}
fs.SetMap(merged)
fs.SetName(saveName)
if len(fs.Map()) != 0 {
// if the patch deletes the object
// don't reset the name
fs.SetName(saveName)
}
return nil
}

View File

@@ -45,7 +45,7 @@ func (jmp *jsonMergePatch) findConflict(
if i == conflictingPatchIdx {
continue
}
if !patches[conflictingPatchIdx].OrgId().GvknEquals(patch.OrgId()) {
if !patches[conflictingPatchIdx].OrgId().Equals(patch.OrgId()) {
continue
}
conflict, err := mergepatch.HasConflicts(
@@ -105,7 +105,7 @@ func (smp *strategicMergePatch) findConflict(
if i == conflictingPatchIdx {
continue
}
if !patches[conflictingPatchIdx].OrgId().GvknEquals(patch.OrgId()) {
if !patches[conflictingPatchIdx].OrgId().Equals(patch.OrgId()) {
continue
}
conflict, err := strategicpatch.MergingMapsHaveConflicts(
@@ -135,7 +135,7 @@ func MergePatches(patches []*resource.Resource,
rc := resmap.New()
for ix, patch := range patches {
id := patch.OrgId()
existing := rc.GetMatchingResourcesByOriginalId(id.GvknEquals)
existing := rc.GetMatchingResourcesByOriginalId(id.Equals)
if len(existing) == 0 {
rc.Append(patch)
continue

View File

@@ -64,8 +64,14 @@ func (ra *ResAccumulator) GetTransformerConfig() *config.TransformerConfig {
func (ra *ResAccumulator) MergeVars(incoming []types.Var) error {
for _, v := range incoming {
matched := ra.resMap.GetMatchingResourcesByOriginalId(
resid.NewResId(v.ObjRef.GVK(), v.ObjRef.Name).GvknEquals)
targetId := resid.NewResIdWithNamespace(v.ObjRef.GVK(), v.ObjRef.Name, v.ObjRef.Namespace)
idMatcher := targetId.GvknEquals
if targetId.Namespace != "" || !targetId.IsNamespaceableKind() {
// Preserve backward compatibility. An empty namespace means
// wildcard search on the namespace hence we still use GvknEquals
idMatcher = targetId.Equals
}
matched := ra.resMap.GetMatchingResourcesByOriginalId(idMatcher)
if len(matched) > 1 {
return fmt.Errorf(
"found %d resId matches for var %s "+

View File

@@ -61,11 +61,13 @@ func determineFieldOrder() []string {
"CommonAnnotations",
"PatchesStrategicMerge",
"PatchesJson6902",
"Patches",
"ConfigMapGenerator",
"SecretGenerator",
"GeneratorOptions",
"Vars",
"Images",
"Replicas",
"Configurations",
"Generators",
"Transformers",
@@ -73,9 +75,7 @@ func determineFieldOrder() []string {
}
// Add deprecated fields here.
deprecated := map[string]bool{
"Patches": true,
}
deprecated := map[string]bool{}
// Account for the inlined TypeMeta fields.
var result []string

View File

@@ -40,11 +40,13 @@ func TestFieldOrder(t *testing.T) {
"CommonAnnotations",
"PatchesStrategicMerge",
"PatchesJson6902",
"Patches",
"ConfigMapGenerator",
"SecretGenerator",
"GeneratorOptions",
"Vars",
"Images",
"Replicas",
"Configurations",
"Generators",
"Transformers",
@@ -264,3 +266,85 @@ generatorOptions:
string(expected), string(bytes))
}
}
func TestFixPatchesField(t *testing.T) {
kustomizationContentWithComments := []byte(`
patches:
- patch1.yaml
- patch2.yaml
`)
expected := []byte(`
patchesStrategicMerge:
- patch1.yaml
- patch2.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
`)
fSys := fs.MakeFakeFS()
fSys.WriteTestKustomizationWith(kustomizationContentWithComments)
mf, err := NewKustomizationFile(fSys)
if err != nil {
t.Fatalf("Unexpected Error: %v", err)
}
kustomization, err := mf.Read()
if err != nil {
t.Fatalf("Unexpected Error: %v", err)
}
if err = mf.Write(kustomization); err != nil {
t.Fatalf("Unexpected Error: %v", err)
}
bytes, _ := fSys.ReadFile(mf.path)
if string(expected) != string(bytes) {
t.Fatalf(
"expected =\n%s\n\nactual =\n%s\n",
string(expected), string(bytes))
}
}
func TestFixPatchesFieldForExtendedPatch(t *testing.T) {
kustomizationContentWithComments := []byte(`
patches:
- path: patch1.yaml
target:
kind: Deployment
- path: patch2.yaml
target:
kind: Service
`)
expected := []byte(`
patches:
- path: patch1.yaml
target:
kind: Deployment
- path: patch2.yaml
target:
kind: Service
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
`)
fSys := fs.MakeFakeFS()
fSys.WriteTestKustomizationWith(kustomizationContentWithComments)
mf, err := NewKustomizationFile(fSys)
if err != nil {
t.Fatalf("Unexpected Error: %v", err)
}
kustomization, err := mf.Read()
if err != nil {
t.Fatalf("Unexpected Error: %v", err)
}
if err = mf.Write(kustomization); err != nil {
t.Fatalf("Unexpected Error: %v", err)
}
bytes, _ := fSys.ReadFile(mf.path)
if string(expected) != string(bytes) {
t.Fatalf(
"expected =\n%s\n\nactual =\n%s\n",
string(expected), string(bytes))
}
}

View File

@@ -35,14 +35,14 @@ func ClonerUsingGitExec(repoSpec *RepoSpec) error {
if err != nil {
return errors.Wrap(err, "no 'git' program on path")
}
repoSpec.cloneDir, err = fs.NewTmpConfirmedDir()
repoSpec.Dir, err = fs.NewTmpConfirmedDir()
if err != nil {
return err
}
cmd := exec.Command(
gitProgram,
"init",
repoSpec.cloneDir.String())
repoSpec.Dir.String())
var out bytes.Buffer
cmd.Stdout = &out
err = cmd.Run()
@@ -50,7 +50,7 @@ func ClonerUsingGitExec(repoSpec *RepoSpec) error {
return errors.Wrapf(
err,
"trouble initializing empty git repo in %s",
repoSpec.cloneDir.String())
repoSpec.Dir.String())
}
cmd = exec.Command(
@@ -60,7 +60,7 @@ func ClonerUsingGitExec(repoSpec *RepoSpec) error {
"origin",
repoSpec.CloneSpec())
cmd.Stdout = &out
cmd.Dir = repoSpec.cloneDir.String()
cmd.Dir = repoSpec.Dir.String()
err = cmd.Run()
if err != nil {
return errors.Wrapf(
@@ -68,20 +68,20 @@ func ClonerUsingGitExec(repoSpec *RepoSpec) error {
"trouble adding remote %s",
repoSpec.CloneSpec())
}
if repoSpec.ref == "" {
repoSpec.ref = "master"
if repoSpec.Ref == "" {
repoSpec.Ref = "master"
}
cmd = exec.Command(
gitProgram,
"fetch",
"--depth=1",
"origin",
repoSpec.ref)
repoSpec.Ref)
cmd.Stdout = &out
cmd.Dir = repoSpec.cloneDir.String()
cmd.Dir = repoSpec.Dir.String()
err = cmd.Run()
if err != nil {
return errors.Wrapf(err, "trouble fetching %s", repoSpec.ref)
return errors.Wrapf(err, "trouble fetching %s", repoSpec.Ref)
}
cmd = exec.Command(
@@ -90,11 +90,11 @@ func ClonerUsingGitExec(repoSpec *RepoSpec) error {
"--hard",
"FETCH_HEAD")
cmd.Stdout = &out
cmd.Dir = repoSpec.cloneDir.String()
cmd.Dir = repoSpec.Dir.String()
err = cmd.Run()
if err != nil {
return errors.Wrapf(
err, "trouble hard resetting empty repository to %s", repoSpec.ref)
err, "trouble hard resetting empty repository to %s", repoSpec.Ref)
}
return nil
}
@@ -105,7 +105,7 @@ func ClonerUsingGitExec(repoSpec *RepoSpec) error {
// used in a test.
func DoNothingCloner(dir fs.ConfirmedDir) Cloner {
return func(rs *RepoSpec) error {
rs.cloneDir = dir
rs.Dir = dir
return nil
}
}

View File

@@ -39,36 +39,36 @@ type RepoSpec struct {
raw string
// Host, e.g. github.com
host string
Host string
// orgRepo name (organization/repoName),
// e.g. kubernetes-sigs/kustomize
orgRepo string
OrgRepo string
// ConfirmedDir where the orgRepo is cloned to.
cloneDir fs.ConfirmedDir
// Dir where the orgRepo is cloned to.
Dir fs.ConfirmedDir
// Relative path in the repository, and in the cloneDir,
// to a Kustomization.
path string
Path string
// Branch or tag reference.
ref string
Ref string
// e.g. .git or empty in case of _git is present
gitSuffix string
GitSuffix string
}
// CloneSpec returns a string suitable for "git clone {spec}".
func (x *RepoSpec) CloneSpec() string {
if isAzureHost(x.host) || isAWSHost(x.host) {
return x.host + x.orgRepo
if isAzureHost(x.Host) || isAWSHost(x.Host) {
return x.Host + x.OrgRepo
}
return x.host + x.orgRepo + x.gitSuffix
return x.Host + x.OrgRepo + x.GitSuffix
}
func (x *RepoSpec) CloneDir() fs.ConfirmedDir {
return x.cloneDir
return x.Dir
}
func (x *RepoSpec) Raw() string {
@@ -76,11 +76,11 @@ func (x *RepoSpec) Raw() string {
}
func (x *RepoSpec) AbsPath() string {
return x.cloneDir.Join(x.path)
return x.Dir.Join(x.Path)
}
func (x *RepoSpec) Cleaner(fSys fs.FileSystem) func() error {
return func() error { return fSys.RemoveAll(x.cloneDir.String()) }
return func() error { return fSys.RemoveAll(x.Dir.String()) }
}
// From strings like git@github.com:someOrg/someRepo.git or
@@ -98,8 +98,8 @@ func NewRepoSpecFromUrl(n string) (*RepoSpec, error) {
return nil, fmt.Errorf("url lacks host: %s", n)
}
return &RepoSpec{
raw: n, host: host, orgRepo: orgRepo,
cloneDir: notCloned, path: path, ref: gitRef, gitSuffix: gitSuffix}, nil
raw: n, Host: host, OrgRepo: orgRepo,
Dir: notCloned, Path: path, Ref: gitRef, GitSuffix: gitSuffix}, nil
}
const (

View File

@@ -72,17 +72,17 @@ func TestNewRepoSpecFromUrl(t *testing.T) {
if err != nil {
t.Errorf("problem %v", err)
}
if rs.host != hostSpec {
bad = append(bad, []string{"host", uri, rs.host, hostSpec})
if rs.Host != hostSpec {
bad = append(bad, []string{"host", uri, rs.Host, hostSpec})
}
if rs.orgRepo != orgRepo {
bad = append(bad, []string{"orgRepo", uri, rs.orgRepo, orgRepo})
if rs.OrgRepo != orgRepo {
bad = append(bad, []string{"orgRepo", uri, rs.OrgRepo, orgRepo})
}
if rs.path != pathName {
bad = append(bad, []string{"path", uri, rs.path, pathName})
if rs.Path != pathName {
bad = append(bad, []string{"path", uri, rs.Path, pathName})
}
if rs.ref != hrefArg {
bad = append(bad, []string{"ref", uri, rs.ref, hrefArg})
if rs.Ref != hrefArg {
bad = append(bad, []string{"ref", uri, rs.Ref, hrefArg})
}
}
}
@@ -201,9 +201,9 @@ func TestNewRepoSpecFromUrl_CloneSpecs(t *testing.T) {
t.Errorf("AbsPath expected to be %v, but got %v on %s",
testcase.absPath, rs.AbsPath(), testcase.input)
}
if rs.ref != testcase.ref {
if rs.Ref != testcase.ref {
t.Errorf("ref expected to be %v, but got %v on %s",
testcase.ref, rs.ref, testcase.input)
testcase.ref, rs.Ref, testcase.input)
}
}
}

View File

@@ -81,6 +81,7 @@ func (x Gvk) Equals(o Gvk) bool {
// In some cases order just specified to provide determinism.
var orderFirst = []string{
"Namespace",
"ResourceQuota",
"StorageClass",
"CustomResourceDefinition",
"MutatingWebhookConfiguration",

View File

@@ -138,11 +138,16 @@ func (th *KustTestHarness) ErrorFromLoadAndRunTransformer(
func (th *KustTestHarness) RunTransformer(
config, input string) (resmap.ResMap, error) {
transConfig, err := th.rf.RF().FromBytes([]byte(config))
resMap, err := th.rf.NewResMapFromBytes([]byte(input))
if err != nil {
th.t.Fatalf("Err: %v", err)
}
resMap, err := th.rf.NewResMapFromBytes([]byte(input))
return th.RunTransformerFromResMap(config, resMap)
}
func (th *KustTestHarness) RunTransformerFromResMap(
config string, resMap resmap.ResMap) (resmap.ResMap, error) {
transConfig, err := th.rf.RF().FromBytes([]byte(config))
if err != nil {
th.t.Fatalf("Err: %v", err)
}

View File

@@ -552,35 +552,27 @@ func (m *resWrangler) makeCopy(copier resCopier) ResMap {
// SubsetThatCouldBeReferencedByResource implements ResMap.
func (m *resWrangler) SubsetThatCouldBeReferencedByResource(
inputRes *resource.Resource) ResMap {
inputId := inputRes.OrgId()
if !inputId.IsNamespaceableKind() {
if inputRes.GetOutermostNamePrefix() == "" {
return m
}
result := New()
for _, r := range m.Resources() {
if r.GetOutermostNamePrefix() == inputRes.GetOutermostNamePrefix() &&
r.GetOutermostNameSuffix() == inputRes.GetOutermostNameSuffix() {
err := result.Append(r)
if err != nil {
panic(err)
}
}
}
return result
}
result := New()
result := newOne()
inputId := inputRes.CurId()
isInputIdNamespaceable := inputId.IsNamespaceableKind()
rctxm := inputRes.PrefixesSuffixesEquals
for _, r := range m.Resources() {
if !r.OrgId().IsNamespaceableKind() || inputRes.InSameFuzzyNamespace(r) {
err := result.Append(r)
if err != nil {
panic(err)
}
// Need to match more accuratly both at the time of selection and transformation.
// OutmostPrefixSuffixEquals is not accurate enough since it is only using
// the outer most suffix and the last prefix. Use PrefixedSuffixesEquals instead.
resId := r.CurId()
if (!isInputIdNamespaceable || !resId.IsNamespaceableKind() || resId.IsNsEquals(inputId)) &&
r.InSameKustomizeCtx(rctxm) {
result.append(r)
}
}
return result
}
func (m *resWrangler) append(res *resource.Resource) {
m.rList = append(m.rList, res)
}
// AppendAll implements ResMap.
func (m *resWrangler) AppendAll(other ResMap) error {
if other == nil {
@@ -659,11 +651,18 @@ func (m *resWrangler) appendReplaceOrMerge(
return nil
}
func anchorRegex(pattern string) string {
if pattern == "" {
return pattern
}
return "^" + pattern + "$"
}
// Select returns a list of resources that
// are selected by a Selector
func (m *resWrangler) Select(s types.Selector) ([]*resource.Resource, error) {
ns := regexp.MustCompile(s.Namespace)
nm := regexp.MustCompile(s.Name)
ns := regexp.MustCompile(anchorRegex(s.Namespace))
nm := regexp.MustCompile(anchorRegex(s.Name))
var result []*resource.Resource
for _, r := range m.Resources() {
curId := r.CurId()

View File

@@ -356,7 +356,7 @@ func TestSubsetThatCouldBeReferencedByResource(t *testing.T) {
r4 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "charlie",
"namespace": "happy",
@@ -365,7 +365,7 @@ func TestSubsetThatCouldBeReferencedByResource(t *testing.T) {
r5 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "Deployment",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "charlie",
"namespace": "happy",
@@ -408,12 +408,12 @@ func TestSubsetThatCouldBeReferencedByResource(t *testing.T) {
"happy namespace no prefix": {
filter: r3,
expected: resmaptest_test.NewRmBuilder(t, rf).
AddR(r3).AddR(r4).AddR(r7).ResMap(),
AddR(r3).AddR(r4).AddR(r5).AddR(r6).AddR(r7).ResMap(),
},
"happy namespace with prefix": {
filter: r5,
expected: resmaptest_test.NewRmBuilder(t, rf).
AddR(r5).AddR(r6).AddR(r7).ResMap(),
AddR(r3).AddR(r4).AddR(r5).AddR(r6).AddR(r7).ResMap(),
},
"cluster level": {
filter: r7,

View File

@@ -41,6 +41,12 @@ metadata:
app: name3
annotations:
bar: baz
---
apiVersion: group1/v1
kind: Kind2
metadata:
name: x-name1
namespace: x-default
`))
if err != nil {
t.Fatalf("unexpected error %v", err)
@@ -56,13 +62,13 @@ func TestFindPatchTargets(t *testing.T) {
}{
{
target: types.Selector{
Name: "name*",
Name: "name.*",
},
count: 3,
},
{
target: types.Selector{
Name: "name*",
Name: "name.*",
AnnotationSelector: "foo=bar",
},
count: 2,
@@ -78,7 +84,7 @@ func TestFindPatchTargets(t *testing.T) {
Gvk: gvk.Gvk{
Kind: "Kind1",
},
Name: "name*",
Name: "name.*",
},
count: 2,
},
@@ -92,7 +98,7 @@ func TestFindPatchTargets(t *testing.T) {
target: types.Selector{
Name: "",
},
count: 3,
count: 4,
},
{
target: types.Selector{
@@ -104,18 +110,60 @@ func TestFindPatchTargets(t *testing.T) {
target: types.Selector{
Namespace: "",
},
count: 3,
count: 4,
},
{
target: types.Selector{
Namespace: "default",
Name: "name*",
Name: "name.*",
Gvk: gvk.Gvk{
Kind: "Kind1",
},
},
count: 1,
},
{
target: types.Selector{
Name: "^name.*",
},
count: 3,
},
{
target: types.Selector{
Name: "name.*$",
},
count: 3,
},
{
target: types.Selector{
Name: "^name.*$",
},
count: 3,
},
{
target: types.Selector{
Namespace: "^def.*",
},
count: 2,
},
{
target: types.Selector{
Namespace: "def.*$",
},
count: 2,
},
{
target: types.Selector{
Namespace: "^def.*$",
},
count: 2,
},
{
target: types.Selector{
Namespace: "default",
},
count: 2,
},
}
for _, testcase := range testcases {
actual, err := rm.Select(testcase.target)

View File

@@ -27,6 +27,22 @@ type Resource struct {
nameSuffixes []string
}
// ResCtx is an interface describing the contextual added
// kept kustomize in the context of each Resource object.
// Currently mainly the name prefix and name suffix are added.
type ResCtx interface {
AddNamePrefix(p string)
AddNameSuffix(s string)
GetOutermostNamePrefix() string
GetOutermostNameSuffix() string
GetNamePrefixes() []string
GetNameSuffixes() []string
}
// ResCtxMatcher returns true if two Resources are being
// modified in the same kustomize context.
type ResCtxMatcher func(ResCtx) bool
// DeepCopy returns a new copy of resource
func (r *Resource) DeepCopy() *Resource {
rc := &Resource{
@@ -104,14 +120,17 @@ func copyStringSlice(s []string) []string {
return c
}
// Implements ResCtx AddNamePrefix
func (r *Resource) AddNamePrefix(p string) {
r.namePrefixes = append(r.namePrefixes, p)
}
// Implements ResCtx AddNameSuffix
func (r *Resource) AddNameSuffix(s string) {
r.nameSuffixes = append(r.nameSuffixes, s)
}
// Implements ResCtx GetOutermostNamePrefix
func (r *Resource) GetOutermostNamePrefix() string {
if len(r.namePrefixes) == 0 {
return ""
@@ -119,6 +138,7 @@ func (r *Resource) GetOutermostNamePrefix() string {
return r.namePrefixes[len(r.namePrefixes)-1]
}
// Implements ResCtx GetOutermostNameSuffix
func (r *Resource) GetOutermostNameSuffix() string {
if len(r.nameSuffixes) == 0 {
return ""
@@ -126,10 +146,51 @@ func (r *Resource) GetOutermostNameSuffix() string {
return r.nameSuffixes[len(r.nameSuffixes)-1]
}
func (r *Resource) InSameFuzzyNamespace(o *Resource) bool {
return r.CurId().IsNsEquals(o.CurId()) &&
r.GetOutermostNamePrefix() == o.GetOutermostNamePrefix() &&
r.GetOutermostNameSuffix() == o.GetOutermostNameSuffix()
func sameBeginningSubarray(a, b []string) bool {
maxlen := len(b)
if len(a) < len(b) {
maxlen = len(a)
}
if maxlen == 0 {
return true
}
for i, v := range a[0 : maxlen-1] {
if v != b[i] {
return false
}
}
return true
}
// Implements ResCtx GetNamePrefixes
func (r *Resource) GetNamePrefixes() []string {
return r.namePrefixes
}
// Implements ResCtx GetNameSuffixes
func (r *Resource) GetNameSuffixes() []string {
return r.nameSuffixes
}
// OutermostPrefixSuffixEquals returns true if both resources
// outer suffix and prefix matches.
func (r *Resource) OutermostPrefixSuffixEquals(o ResCtx) bool {
return (r.GetOutermostNamePrefix() == o.GetOutermostNamePrefix()) && (r.GetOutermostNameSuffix() == o.GetOutermostNameSuffix())
}
// PrefixesSuffixesEquals is conceptually doing the same task
// as OutermostPrefixSuffix but performs a deeper comparison
// of the the list of suffix and prefix.
func (r *Resource) PrefixesSuffixesEquals(o ResCtx) bool {
return sameBeginningSubarray(r.GetNamePrefixes(), o.GetNamePrefixes()) && sameBeginningSubarray(r.GetNameSuffixes(), o.GetNameSuffixes())
}
// This is used to compute if a referrer could potentially be impacted
// by the change of name of a referral.
func (r *Resource) InSameKustomizeCtx(rctxm ResCtxMatcher) bool {
return rctxm(r)
}
func (r *Resource) GetOriginalName() string {

View File

@@ -923,6 +923,107 @@ spec:
`)
}
func TestExtendedPatchWithoutTarget(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
makeCommonFileForExtendedPatchTest(th)
th.WriteK("/app/base", `
resources:
- deployment.yaml
- service.yaml
patches:
- path: patch.yaml
`)
th.WriteF("/app/base/patch.yaml", `
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: busybox
annotations:
new-key: new-value
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1beta2
kind: Deployment
metadata:
labels:
app: nginx
name: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
volumeMounts:
- mountPath: /tmp/ps
name: nginx-persistent-storage
volumes:
- emptyDir: {}
name: nginx-persistent-storage
- configMap:
name: configmap-in-base
name: configmap-in-base
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
annotations:
new-key: new-value
labels:
app: busybox
name: busybox
spec:
template:
metadata:
labels:
app: busybox
spec:
containers:
- image: busybox
name: busybox
volumeMounts:
- mountPath: /tmp/ps
name: busybox-persistent-storage
volumes:
- emptyDir: {}
name: busybox-persistent-storage
- configMap:
name: configmap-in-base
name: configmap-in-base
---
apiVersion: v1
kind: Service
metadata:
labels:
app: nginx
name: nginx
spec:
ports:
- port: 80
selector:
app: nginx
---
apiVersion: v1
kind: Service
metadata:
labels:
app: busybox
name: busybox
spec:
ports:
- port: 8080
selector:
app: busybox
`)
}
func TestExtendedPatchNoMatchMultiplePatch(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
makeCommonFileForExtendedPatchTest(th)

View File

@@ -0,0 +1,243 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package target_test
import (
"sigs.k8s.io/kustomize/v3/pkg/kusttest"
"testing"
)
func makeResourcesForPatchTest(th *kusttest_test.KustTestHarness) {
th.WriteF("/app/base/deployment.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
volumeMounts:
- name: nginx-persistent-storage
mountPath: /tmp/ps
volumes:
- name: nginx-persistent-storage
emptyDir: {}
- configMap:
name: configmap-in-base
name: configmap-in-base
`)
}
func TestStrategicMergePatchInline(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
makeResourcesForPatchTest(th)
th.WriteK("/app/base", `
resources:
- deployment.yaml
patchesStrategicMerge:
- |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
image: image1
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
name: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: image1
name: nginx
volumeMounts:
- mountPath: /tmp/ps
name: nginx-persistent-storage
volumes:
- emptyDir: {}
name: nginx-persistent-storage
- configMap:
name: configmap-in-base
name: configmap-in-base
`)
}
func TestJSONPatchInline(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
makeResourcesForPatchTest(th)
th.WriteK("/app/base", `
resources:
- deployment.yaml
patchesJson6902:
- target:
group: apps
version: v1
kind: Deployment
name: nginx
patch: |-
- op: replace
path: /spec/template/spec/containers/0/image
value: image1
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
name: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: image1
name: nginx
volumeMounts:
- mountPath: /tmp/ps
name: nginx-persistent-storage
volumes:
- emptyDir: {}
name: nginx-persistent-storage
- configMap:
name: configmap-in-base
name: configmap-in-base
`)
}
func TestExtendedPatchInlineJSON(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
makeResourcesForPatchTest(th)
th.WriteK("/app/base", `
resources:
- deployment.yaml
patches:
- target:
kind: Deployment
name: nginx
patch: |-
- op: replace
path: /spec/template/spec/containers/0/image
value: image1
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
name: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: image1
name: nginx
volumeMounts:
- mountPath: /tmp/ps
name: nginx-persistent-storage
volumes:
- emptyDir: {}
name: nginx-persistent-storage
- configMap:
name: configmap-in-base
name: configmap-in-base
`)
}
func TestExtendedPatchInlineYAML(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/app/base")
makeResourcesForPatchTest(th)
th.WriteK("/app/base", `
resources:
- deployment.yaml
patches:
- target:
kind: Deployment
name: nginx
patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
template:
spec:
containers:
- name: nginx
image: image1
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
name: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: image1
name: nginx
volumeMounts:
- mountPath: /tmp/ps
name: nginx-persistent-storage
volumes:
- emptyDir: {}
name: nginx-persistent-storage
- configMap:
name: configmap-in-base
name: configmap-in-base
`)
}

View File

@@ -156,7 +156,7 @@ func (kt *KustTarget) configureBuiltinPatchJson6902Transformer(
for _, args := range kt.kustomization.PatchesJson6902 {
c.Target = *args.Target
c.Path = args.Path
c.JsonOp = "" // Not implemented for kustomization file yet.
c.JsonOp = args.Patch
p := builtin.NewPatchJson6902TransformerPlugin()
err = kt.configureBuiltinPlugin(p, c, "patchJson6902")
if err != nil {
@@ -178,7 +178,6 @@ func (kt *KustTarget) configureBuiltinPatchStrategicMergeTransformer(
Patches string `json:"patches,omitempty" yaml:"patches,omitempty"`
}
c.Paths = kt.kustomization.PatchesStrategicMerge
c.Patches = "" // Not implemented for kustomization file yet
p := builtin.NewPatchStrategicMergeTransformerPlugin()
err = kt.configureBuiltinPlugin(p, c, "patchStrategicMerge")
if err != nil {
@@ -200,7 +199,7 @@ func (kt *KustTarget) configureBuiltinPatchTransformer(
Target *types.Selector `json:"target,omitempty" yaml:"target,omitempty"`
}
for _, patch := range kt.kustomization.Patches {
c.Target = &patch.Target
c.Target = patch.Target
c.Patch = patch.Patch
c.Path = patch.Path
p := builtin.NewPatchTransformerPlugin()

View File

@@ -6,7 +6,6 @@ package target_test
import (
"encoding/base64"
"fmt"
"reflect"
"strings"
"testing"
@@ -334,7 +333,10 @@ vars:
t.Fatalf("unexpected size %d", len(vars))
}
for i := range vars[:2] {
if !reflect.DeepEqual(vars[i], someVars[i]) {
// By using Var.DeepEqual, we are protecting the code
// from a potential invocation of vars[i].ObjRef.GVK()
// during AccumulateTarget
if !vars[i].DeepEqual(someVars[i]) {
t.Fatalf("unexpected var[%d]:\n %v\n %v", i, vars[i], someVars[i])
}
}
@@ -387,7 +389,10 @@ resources:
t.Fatalf("expected 4 vars, got %d", len(vars))
}
for i := range vars {
if !reflect.DeepEqual(vars[i], someVars[i]) {
// By using Var.DeepEqual, we are protecting the code
// from a potential invocation of vars[i].ObjRef.GVK()
// during AccumulateTarget
if !vars[i].DeepEqual(someVars[i]) {
t.Fatalf("unexpected var[%d]:\n %v\n %v", i, vars[i], someVars[i])
}
}

View File

@@ -5,6 +5,7 @@ package target_test
import (
"sigs.k8s.io/kustomize/v3/pkg/kusttest"
"strings"
"testing"
)
@@ -93,3 +94,508 @@ rules:
- get
`)
}
// TestNameAndNsTransformation validates that NamespaceTransformer,
// PrefixSuffixTransformer and namereference transformers are
// able to deal with simultaneous change of namespace and name.
func TestNameAndNsTransformation(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/nameandns")
th.WriteK("/nameandns", `
namePrefix: p1-
nameSuffix: -s1
namespace: newnamespace
resources:
- resources.yaml
`)
th.WriteF("/nameandns/resources.yaml", `
apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
namespace: ns1
---
apiVersion: v1
kind: Service
metadata:
name: svc1
namespace: ns1
---
apiVersion: v1
kind: Service
metadata:
name: svc2
namespace: ns1
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: sa1
namespace: ns1
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: sa2
namespace: ns1
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: manager-rolebinding
subjects:
- kind: ServiceAccount
name: sa1
namespace: ns1
- kind: ServiceAccount
name: sa2
namespace: ns1
- kind: ServiceAccount
name: sa3
namespace: random
- kind: ServiceAccount
name: default
namespace: irrelevant
---
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: example
webhooks:
- name: example1
clientConfig:
service:
name: svc1
namespace: ns1
- name: example2
clientConfig:
service:
name: svc2
namespace: ns1
- name: example3
clientConfig:
service:
name: svc3
namespace: random
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: crds.my.org
---
kind: ClusterRole
metadata:
name: cr1
---
kind: ClusterRoleBinding
metadata:
name: crb1
subjects:
- kind: ServiceAccount
name: default
namespace: irrelevant
---
kind: PersistentVolume
metadata:
name: pv1
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
kind: ConfigMap
metadata:
name: p1-cm1-s1
namespace: newnamespace
---
apiVersion: v1
kind: ConfigMap
metadata:
name: p1-cm2-s1
namespace: newnamespace
---
apiVersion: v1
kind: Service
metadata:
name: p1-svc1-s1
namespace: newnamespace
---
apiVersion: v1
kind: Service
metadata:
name: p1-svc2-s1
namespace: newnamespace
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: p1-sa1-s1
namespace: newnamespace
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: p1-sa2-s1
namespace: newnamespace
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: p1-manager-rolebinding-s1
subjects:
- kind: ServiceAccount
name: p1-sa1-s1
namespace: newnamespace
- kind: ServiceAccount
name: p1-sa2-s1
namespace: newnamespace
- kind: ServiceAccount
name: sa3
namespace: random
- kind: ServiceAccount
name: default
namespace: newnamespace
---
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: p1-example-s1
webhooks:
- clientConfig:
service:
name: p1-svc1-s1
namespace: newnamespace
name: example1
- clientConfig:
service:
name: p1-svc2-s1
namespace: newnamespace
name: example2
- clientConfig:
service:
name: svc3
namespace: random
name: example3
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: crds.my.org
---
kind: ClusterRole
metadata:
name: p1-cr1-s1
---
kind: ClusterRoleBinding
metadata:
name: p1-crb1-s1
subjects:
- kind: ServiceAccount
name: default
namespace: newnamespace
---
kind: PersistentVolume
metadata:
name: p1-pv1-s1
`)
}
// This serie of constants is used to prove the need of
// the namespace field in the objref field of the var declaration.
// The following tests demonstrate that it creates umbiguous variable
// declaration if two entities of the kind with the same name
// but in different namespaces are declared.
// This is tracking the following issue:
// https://github.com/kubernetes-sigs/kustomize/issues/1298
const namespaceNeedInVarMyApp string = `
resources:
- elasticsearch-dev-service.yaml
- elasticsearch-test-service.yaml
vars:
- name: elasticsearch-test-service-name
objref:
kind: Service
name: elasticsearch
apiVersion: v1
fieldref:
fieldpath: metadata.name
- name: elasticsearch-test-protocol
objref:
kind: Service
name: elasticsearch
apiVersion: v1
fieldref:
fieldpath: spec.ports[0].protocol
- name: elasticsearch-dev-service-name
objref:
kind: Service
name: elasticsearch
apiVersion: v1
fieldref:
fieldpath: metadata.name
- name: elasticsearch-dev-protocol
objref:
kind: Service
name: elasticsearch
apiVersion: v1
fieldref:
fieldpath: spec.ports[0].protocol
`
const namespaceNeedInVarDevResources string = `
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: elasticsearch
namespace: dev
spec:
template:
spec:
containers:
- name: elasticsearch
env:
- name: DISCOVERY_SERVICE
value: "$(elasticsearch-dev-service-name).monitoring.svc.cluster.local"
- name: DISCOVERY_PROTOCOL
value: "$(elasticsearch-dev-protocol)"
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch
namespace: dev
spec:
ports:
- name: transport
port: 9300
protocol: TCP
clusterIP: None
`
const namespaceNeedInVarTestResources string = `
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: elasticsearch
namespace: test
spec:
template:
spec:
containers:
- name: elasticsearch
env:
- name: DISCOVERY_SERVICE
value: "$(elasticsearch-test-service-name).monitoring.svc.cluster.local"
- name: DISCOVERY_PROTOCOL
value: "$(elasticsearch-test-protocol)"
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch
namespace: test
spec:
ports:
- name: transport
port: 9300
protocol: UDP
clusterIP: None
`
const namespaceNeedInVarExpectedOutput string = `
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: elasticsearch
namespace: dev
spec:
template:
spec:
containers:
- env:
- name: DISCOVERY_SERVICE
value: elasticsearch.monitoring.svc.cluster.local
- name: DISCOVERY_PROTOCOL
value: TCP
name: elasticsearch
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch
namespace: dev
spec:
clusterIP: None
ports:
- name: transport
port: 9300
protocol: TCP
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: elasticsearch
namespace: test
spec:
template:
spec:
containers:
- env:
- name: DISCOVERY_SERVICE
value: elasticsearch.monitoring.svc.cluster.local
- name: DISCOVERY_PROTOCOL
value: UDP
name: elasticsearch
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch
namespace: test
spec:
clusterIP: None
ports:
- name: transport
port: 9300
protocol: UDP
`
// TestVariablesAmbiguous demonstrates how two variables pointing at two different resources
// using the same name in different namespaces are treated as ambiguous if the namespace is
// not specified
func TestVariablesAmbiguous(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/namespaceNeedInVar/myapp")
th.WriteK("/namespaceNeedInVar/myapp", namespaceNeedInVarMyApp)
th.WriteF("/namespaceNeedInVar/myapp/elasticsearch-dev-service.yaml", namespaceNeedInVarDevResources)
th.WriteF("/namespaceNeedInVar/myapp/elasticsearch-test-service.yaml", namespaceNeedInVarTestResources)
_, err := th.MakeKustTarget().MakeCustomizedResMap()
if err == nil {
t.Fatalf("expected error")
}
if !strings.Contains(err.Error(), "unable to disambiguate") {
t.Fatalf("unexpected error %v", err)
}
}
const namespaceNeedInVarDevFolder string = `
resources:
- elasticsearch-dev-service.yaml
vars:
- name: elasticsearch-dev-service-name
objref:
kind: Service
name: elasticsearch
apiVersion: v1
fieldref:
fieldpath: metadata.name
- name: elasticsearch-dev-protocol
objref:
kind: Service
name: elasticsearch
apiVersion: v1
fieldref:
fieldpath: spec.ports[0].protocol
`
const namespaceNeedInVarTestFolder string = `
resources:
- elasticsearch-test-service.yaml
vars:
- name: elasticsearch-test-service-name
objref:
kind: Service
name: elasticsearch
apiVersion: v1
fieldref:
fieldpath: metadata.name
- name: elasticsearch-test-protocol
objref:
kind: Service
name: elasticsearch
apiVersion: v1
fieldref:
fieldpath: spec.ports[0].protocol
`
// TestVariablesAmbiguousWorkaround demonstrates a possible workaround
// to TestVariablesAmbiguous problem. It requires to separate the variables
// and resources into multiple kustomization context/folders instead of one.
func TestVariablesAmbiguousWorkaround(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/namespaceNeedInVar/workaround")
th.WriteK("/namespaceNeedInVar/dev", namespaceNeedInVarDevFolder)
th.WriteF("/namespaceNeedInVar/dev/elasticsearch-dev-service.yaml", namespaceNeedInVarDevResources)
th.WriteK("/namespaceNeedInVar/test", namespaceNeedInVarTestFolder)
th.WriteF("/namespaceNeedInVar/test/elasticsearch-test-service.yaml", namespaceNeedInVarTestResources)
th.WriteK("/namespaceNeedInVar/workaround", `
resources:
- ../dev
- ../test
`)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, namespaceNeedInVarExpectedOutput)
}
const namespaceNeedInVarMyAppWithNamespace string = `
resources:
- elasticsearch-dev-service.yaml
- elasticsearch-test-service.yaml
vars:
- name: elasticsearch-test-service-name
objref:
kind: Service
name: elasticsearch
namespace: test
apiVersion: v1
fieldref:
fieldpath: metadata.name
- name: elasticsearch-test-protocol
objref:
kind: Service
name: elasticsearch
namespace: test
apiVersion: v1
fieldref:
fieldpath: spec.ports[0].protocol
- name: elasticsearch-dev-service-name
objref:
kind: Service
name: elasticsearch
namespace: dev
apiVersion: v1
fieldref:
fieldpath: metadata.name
- name: elasticsearch-dev-protocol
objref:
kind: Service
name: elasticsearch
namespace: dev
apiVersion: v1
fieldref:
fieldpath: spec.ports[0].protocol
`
// TestVariablesDisambiguatedWithNamespace demonstrates that adding the namespace
// to the variable declarations allows to disambiguate the variables.
func TestVariablesDisambiguatedWithNamespace(t *testing.T) {
th := kusttest_test.NewKustTestHarness(t, "/namespaceNeedInVar/myapp")
th.WriteK("/namespaceNeedInVar/myapp", namespaceNeedInVarMyAppWithNamespace)
th.WriteF("/namespaceNeedInVar/myapp/elasticsearch-dev-service.yaml", namespaceNeedInVarDevResources)
th.WriteF("/namespaceNeedInVar/myapp/elasticsearch-test-service.yaml", namespaceNeedInVarTestResources)
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Err: %v", err)
}
th.AssertActualEqualsExpected(m, namespaceNeedInVarExpectedOutput)
}

View File

@@ -4,7 +4,6 @@
package target_test
import (
"strings"
"testing"
"sigs.k8s.io/kustomize/v3/pkg/kusttest"
@@ -16,6 +15,7 @@ resources:
- serviceaccount.yaml
- rolebinding.yaml
- clusterrolebinding.yaml
- clusterrole.yaml
namePrefix: pfx-
nameSuffix: -sfx
`)
@@ -32,7 +32,7 @@ metadata:
name: rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
kind: ClusterRole
name: role
subjects:
- kind: ServiceAccount
@@ -45,11 +45,21 @@ metadata:
name: rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
kind: ClusterRole
name: role
subjects:
- kind: ServiceAccount
name: serviceaccount
`)
th.WriteF("/app/base/clusterrole.yaml", `
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: role
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "watch", "list"]
`)
}
@@ -97,8 +107,8 @@ metadata:
name: pfx-rolebinding-sfx
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: role
kind: ClusterRole
name: pfx-role-sfx
subjects:
- kind: ServiceAccount
name: pfx-serviceaccount-sfx
@@ -109,11 +119,25 @@ metadata:
name: pfx-rolebinding-sfx
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: role
kind: ClusterRole
name: pfx-role-sfx
subjects:
- kind: ServiceAccount
name: pfx-serviceaccount-sfx
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pfx-role-sfx
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- watch
- list
`)
}
@@ -137,8 +161,8 @@ metadata:
name: a-pfx-rolebinding-sfx-suffixA
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: role
kind: ClusterRole
name: a-pfx-role-sfx-suffixA
subjects:
- kind: ServiceAccount
name: a-pfx-serviceaccount-sfx-suffixA
@@ -149,11 +173,25 @@ metadata:
name: a-pfx-rolebinding-sfx-suffixA
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: role
kind: ClusterRole
name: a-pfx-role-sfx-suffixA
subjects:
- kind: ServiceAccount
name: a-pfx-serviceaccount-sfx-suffixA
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: a-pfx-role-sfx-suffixA
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- watch
- list
`)
}
@@ -177,8 +215,8 @@ metadata:
name: b-pfx-rolebinding-sfx-suffixB
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: role
kind: ClusterRole
name: b-pfx-role-sfx-suffixB
subjects:
- kind: ServiceAccount
name: b-pfx-serviceaccount-sfx-suffixB
@@ -189,11 +227,25 @@ metadata:
name: b-pfx-rolebinding-sfx-suffixB
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: role
kind: ClusterRole
name: b-pfx-role-sfx-suffixB
subjects:
- kind: ServiceAccount
name: b-pfx-serviceaccount-sfx-suffixB
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: b-pfx-role-sfx-suffixB
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- watch
- list
`)
}
@@ -218,8 +270,8 @@ metadata:
name: a-pfx-rolebinding-sfx-suffixA
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: role
kind: ClusterRole
name: a-pfx-role-sfx-suffixA
subjects:
- kind: ServiceAccount
name: a-pfx-serviceaccount-sfx-suffixA
@@ -230,12 +282,26 @@ metadata:
name: a-pfx-rolebinding-sfx-suffixA
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: role
kind: ClusterRole
name: a-pfx-role-sfx-suffixA
subjects:
- kind: ServiceAccount
name: a-pfx-serviceaccount-sfx-suffixA
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: a-pfx-role-sfx-suffixA
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- watch
- list
---
apiVersion: v1
kind: ServiceAccount
metadata:
@@ -247,8 +313,8 @@ metadata:
name: b-pfx-rolebinding-sfx-suffixB
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: role
kind: ClusterRole
name: b-pfx-role-sfx-suffixB
subjects:
- kind: ServiceAccount
name: b-pfx-serviceaccount-sfx-suffixB
@@ -259,11 +325,25 @@ metadata:
name: b-pfx-rolebinding-sfx-suffixB
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: role
kind: ClusterRole
name: b-pfx-role-sfx-suffixB
subjects:
- kind: ServiceAccount
name: b-pfx-serviceaccount-sfx-suffixB
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: b-pfx-role-sfx-suffixB
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- watch
- list
`)
}
@@ -289,12 +369,100 @@ metadata:
name: serviceaccount
`)
_, err := th.MakeKustTarget().MakeCustomizedResMap()
if err == nil {
t.Fatalf("Expected resource conflict.")
}
if !strings.Contains(
err.Error(), "multiple matches for ~G_v1_ServiceAccount") {
m, err := th.MakeKustTarget().MakeCustomizedResMap()
if err != nil {
t.Fatalf("Unexpected err: %v", err)
}
th.AssertActualEqualsExpected(m, `
apiVersion: v1
kind: ServiceAccount
metadata:
name: a-serviceaccount-suffixA
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: a-pfx-serviceaccount-sfx-suffixA
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: a-pfx-rolebinding-sfx-suffixA
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: a-pfx-role-sfx-suffixA
subjects:
- kind: ServiceAccount
name: a-pfx-serviceaccount-sfx-suffixA
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: a-pfx-rolebinding-sfx-suffixA
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: a-pfx-role-sfx-suffixA
subjects:
- kind: ServiceAccount
name: a-pfx-serviceaccount-sfx-suffixA
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: a-pfx-role-sfx-suffixA
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- watch
- list
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: b-pfx-serviceaccount-sfx-suffixB
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: b-pfx-rolebinding-sfx-suffixB
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: b-pfx-role-sfx-suffixB
subjects:
- kind: ServiceAccount
name: b-pfx-serviceaccount-sfx-suffixB
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: b-pfx-rolebinding-sfx-suffixB
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: b-pfx-role-sfx-suffixB
subjects:
- kind: ServiceAccount
name: b-pfx-serviceaccount-sfx-suffixB
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: b-pfx-role-sfx-suffixB
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- watch
- list
`)
}

View File

@@ -272,10 +272,10 @@ nameReference:
- path: spec/service/name
kind: APIService
group: apiregistration.k8s.io
- path: webhooks/clientConfig/service/name
- path: webhooks/clientConfig/service
kind: ValidatingWebhookConfiguration
group: admissionregistration.k8s.io
- path: webhooks/clientConfig/service/name
- path: webhooks/clientConfig/service
kind: MutatingWebhookConfiguration
group: admissionregistration.k8s.io

View File

@@ -21,5 +21,9 @@ const (
namespace:
- path: metadata/namespace
create: true
- path: subjects
kind: RoleBinding
- path: subjects
kind: ClusterRoleBinding
`
)

View File

@@ -191,6 +191,11 @@ func (o *nameReferenceTransformer) getNameAndNsStruct(
return nil, err
}
if (newname == oldName) && (newnamespace == nil) {
// no candidate found.
return inMap, nil
}
inMap["name"] = newname
if newnamespace != "" {
// We don't want value "" to replace value "default" since
@@ -212,6 +217,12 @@ func (o *nameReferenceTransformer) getNewNameFunc(
oldName, _ := in.(string)
return o.getSimpleNameField(oldName, referrer, target,
referralCandidates, referralCandidates.Resources())
case map[string]interface{}:
// Kind: ValidatingWebhookConfiguration
// FieldSpec is webhooks/clientConfig/service
oldMap, _ := in.(map[string]interface{})
return o.getNameAndNsStruct(oldMap, referrer, target,
referralCandidates)
case []interface{}:
l, _ := in.([]interface{})
for idx, item := range l {

View File

@@ -520,7 +520,7 @@ func TestNameReferenceUnhappyRun(t *testing.T) {
},
},
}).ResMap(),
expectedErr: "is expected to be"},
expectedErr: "is expected to contain a name field"},
}
nrt := NewNameReferenceTransformer(defaultTransformerConfig.NameReference)
@@ -652,6 +652,7 @@ const (
ns1 = "ns1"
ns2 = "ns2"
ns3 = "ns3"
ns4 = "ns4"
orgname = "uniquename"
prefixedname = "prefix-uniquename"
@@ -808,6 +809,11 @@ func TestNameReferenceClusterWide(t *testing.T) {
"name": orgname,
"namespace": ns2,
},
map[string]interface{}{
"kind": "ServiceAccount",
"name": orgname,
"namespace": "random",
},
}}).ResMap()
expected := resmaptest_test.NewSeededRmBuilder(t, rf, m.ShallowCopy()).
@@ -862,6 +868,11 @@ func TestNameReferenceClusterWide(t *testing.T) {
"name": suffixedname,
"namespace": ns2,
},
map[string]interface{}{
"kind": "ServiceAccount",
"name": orgname,
"namespace": "random",
},
},
}).ResMap()
@@ -890,6 +901,13 @@ func TestNameReferenceNamespaceTransformation(t *testing.T) {
rf := resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl())
m := resmaptest_test.NewRmBuilder(t, rf).
AddWithNsAndName(ns4, orgname, map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": orgname,
"namespace": ns4,
}}).
// Add ServiceAccount with the same org name in "ns1" namespaces
AddWithNsAndName(ns1, orgname, map[string]interface{}{
"apiVersion": "v1",
@@ -934,6 +952,16 @@ func TestNameReferenceNamespaceTransformation(t *testing.T) {
"name": orgname,
"namespace": ns3,
},
map[string]interface{}{
"kind": "ServiceAccount",
"name": orgname,
"namespace": "random",
},
map[string]interface{}{
"kind": "ServiceAccount",
"name": orgname,
"namespace": ns4,
},
}}).ResMap()
expected := resmaptest_test.NewSeededRmBuilder(t, rf, m.ShallowCopy()).
@@ -963,6 +991,16 @@ func TestNameReferenceNamespaceTransformation(t *testing.T) {
"name": suffixedname,
"namespace": ns2,
},
map[string]interface{}{
"kind": "ServiceAccount",
"name": orgname,
"namespace": "random",
},
map[string]interface{}{
"kind": "ServiceAccount",
"name": orgname,
"namespace": ns4,
},
},
}).ResMap()

55
pkg/types/fix.go Normal file
View File

@@ -0,0 +1,55 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package types holds struct definitions that should find a better home.
package types
import (
"log"
"regexp"
"sigs.k8s.io/yaml"
)
// FixKustomizationPreUnmarshalling modies the raw data
// before marshalling - e.g. changes old field names to
// new field names.
func FixKustomizationPreUnmarshalling(data []byte) []byte {
deprecateFieldsMap := map[string]string{
"imageTags:": "images:",
}
for oldname, newname := range deprecateFieldsMap {
pattern := regexp.MustCompile(oldname)
data = pattern.ReplaceAll(data, []byte(newname))
}
if useLegacyPatch(data) {
pattern := regexp.MustCompile("patches:")
data = pattern.ReplaceAll(data, []byte("patchesStrategicMerge:"))
}
return data
}
func useLegacyPatch(data []byte) bool {
found := false
var object map[string]interface{}
err := yaml.Unmarshal(data, &object)
if err != nil {
log.Fatalf("invalid content from %s\n", string(data))
}
if rawPatches, ok := object["patches"]; ok {
patches, ok := rawPatches.([]interface{})
if !ok {
log.Fatalf("invalid patches from %v\n", rawPatches)
}
for _, p := range patches {
_, ok := p.(string)
if ok {
found = true
}
}
}
return found
}

View File

@@ -5,8 +5,6 @@
package types
import (
"regexp"
"sigs.k8s.io/kustomize/v3/pkg/gvk"
"sigs.k8s.io/kustomize/v3/pkg/image"
)
@@ -195,20 +193,6 @@ func (k *Kustomization) EnforceFields() []string {
return errs
}
// FixKustomizationPreUnmarshalling modies the raw data
// before marshalling - e.g. changes old field names to
// new field names.
func FixKustomizationPreUnmarshalling(data []byte) []byte {
deprecateFieldsMap := map[string]string{
"imageTags:": "images:",
}
for oldname, newname := range deprecateFieldsMap {
pattern := regexp.MustCompile(oldname)
data = pattern.ReplaceAll(data, []byte(newname))
}
return data
}
// GeneratorArgs contains arguments common to generators.
type GeneratorArgs struct {
// Namespace for the configmap, optional
@@ -347,6 +331,9 @@ type PatchJson6902 struct {
// relative file path for a json patch file inside a kustomization
Path string `json:"path,omitempty" yaml:"path,omitempty"`
// inline patch string
Patch string `json:"patch,omitempty" yaml:"patch,omitempty"`
}
// PatchTarget represents the kubernetes object that the patch is applied to
@@ -385,7 +372,7 @@ type Patch struct {
Patch string `json:"patch,omitempty" yaml:"patch,omitempty"`
// Target points to the resources that the patch is applied to
Target Selector `json:"target,omitempty" yaml:"target,omitempty"`
Target *Selector `json:"target,omitempty" yaml:"target,omitempty"`
}
// Selector specifies a set of resources.

View File

@@ -18,6 +18,7 @@ package types
import (
"fmt"
"reflect"
"sort"
"strings"
@@ -54,6 +55,7 @@ type Target struct {
APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"`
gvk.Gvk `json:",inline,omitempty" yaml:",inline,omitempty"`
Name string `json:"name" yaml:"name"`
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
}
// FieldSelector contains the fieldPath to an object field.
@@ -64,10 +66,22 @@ type FieldSelector struct {
}
// defaulting sets reference to field used by default.
func (v *Var) defaulting() {
func (v *Var) Defaulting() {
if v.FieldRef.FieldPath == "" {
v.FieldRef.FieldPath = defaultFieldPath
}
v.ObjRef.GVK()
}
// DeepEqual returns true if var a and b are Equals.
// Note 1: The objects are unchanged by the VarEqual
// Note 2: Should be normalize be FieldPath before doing
// the DeepEqual. spec.a[b] is supposed to be the same
// as spec.a.b
func (v Var) DeepEqual(other Var) bool {
v.Defaulting()
other.Defaulting()
return reflect.DeepEqual(v, other)
}
// VarSet is a set of Vars where no var.Name is repeated.
@@ -129,11 +143,52 @@ func (vs *VarSet) Merge(v Var) error {
return fmt.Errorf(
"var '%s' already encountered", v.Name)
}
v.defaulting()
v.Defaulting()
vs.set[v.Name] = v
return nil
}
// AbsorbSet absorbs other vars with error on (name,value) collision.
func (vs *VarSet) AbsorbSet(incoming VarSet) error {
for _, v := range incoming.set {
if err := vs.Absorb(v); err != nil {
return err
}
}
return nil
}
// AbsorbSlice absorbs a Var slice with error on (name,value) collision.
// Empty fields in incoming vars are defaulted.
func (vs *VarSet) AbsorbSlice(incoming []Var) error {
for _, v := range incoming {
if err := vs.Absorb(v); err != nil {
return err
}
}
return nil
}
// Absorb absorbs another Var with error on (name,value) collision.
// Empty fields in incoming Var is defaulted.
func (vs *VarSet) Absorb(v Var) error {
conflicting := vs.Get(v.Name)
if conflicting == nil {
// no conflict. The var is valid.
v.Defaulting()
vs.set[v.Name] = v
return nil
}
if !reflect.DeepEqual(v, *conflicting) {
// two vars with the same name are pointing at two
// different resources.
return fmt.Errorf(
"var '%s' already encountered", v.Name)
}
return nil
}
// Contains is true if the set has the other var.
func (vs *VarSet) Contains(other Var) bool {
return vs.Get(other.Name) != nil

View File

@@ -81,7 +81,7 @@ func TestDefaulting(t *testing.T) {
Name: "my-secret",
},
}
v.defaulting()
v.Defaulting()
if v.FieldRef.FieldPath != defaultFieldPath {
t.Fatalf("expected %s, got %v",
defaultFieldPath, v.FieldRef.FieldPath)

View File

@@ -2,7 +2,6 @@
package builtin
import (
"sigs.k8s.io/kustomize/v3/pkg/gvk"
"sigs.k8s.io/kustomize/v3/pkg/ifc"
"sigs.k8s.io/kustomize/v3/pkg/resid"
"sigs.k8s.io/kustomize/v3/pkg/resmap"
@@ -36,23 +35,23 @@ func (p *NamespaceTransformerPlugin) Transform(m resmap.ResMap) error {
return nil
}
for _, r := range m.Resources() {
id := r.OrgId()
fs, ok := p.isSelected(id)
if !ok {
continue
}
if len(r.Map()) == 0 {
// Don't mutate empty objects?
continue
}
if doIt(id, fs) {
if err := p.changeNamespace(r, fs); err != nil {
id := r.OrgId()
applicableFs := p.applicableFieldSpecs(id)
for _, fs := range applicableFs {
err := transformers.MutateField(
r.Map(), fs.PathSlice(), fs.CreateIfNotPresent,
p.changeNamespace(r))
if err != nil {
return err
}
}
}
p.updateClusterRoleBinding(m)
p.updateServiceReference(m)
return nil
}
@@ -62,104 +61,64 @@ const metaNamespace = "metadata/namespace"
// all objects have it, even "ClusterKind" objects
// that don't exist in a namespace (the Namespace
// object itself doesn't live in a namespace).
func doIt(id resid.ResId, fs *config.FieldSpec) bool {
return fs.Path != metaNamespace ||
(fs.Path == metaNamespace && id.IsNamespaceableKind())
}
func (p *NamespaceTransformerPlugin) changeNamespace(
r *resource.Resource, fs *config.FieldSpec) error {
return transformers.MutateField(
r.Map(), fs.PathSlice(), fs.CreateIfNotPresent,
func(_ interface{}) (interface{}, error) {
return p.Namespace, nil
})
}
func (p *NamespaceTransformerPlugin) isSelected(
id resid.ResId) (*config.FieldSpec, bool) {
func (p *NamespaceTransformerPlugin) applicableFieldSpecs(id resid.ResId) []config.FieldSpec {
res := []config.FieldSpec{}
for _, fs := range p.FieldSpecs {
if id.IsSelected(&fs.Gvk) {
return &fs, true
if id.IsSelected(&fs.Gvk) && (fs.Path != metaNamespace || (fs.Path == metaNamespace && id.IsNamespaceableKind())) {
res = append(res, fs)
}
}
return nil, false
return res
}
func (p *NamespaceTransformerPlugin) updateClusterRoleBinding(m resmap.ResMap) {
srvAccount := gvk.Gvk{Version: "v1", Kind: "ServiceAccount"}
saMap := map[string]bool{}
for _, id := range m.AllIds() {
if id.Gvk.Equals(srvAccount) {
saMap[id.Name] = true
}
}
for _, res := range m.Resources() {
if res.OrgId().Kind != "ClusterRoleBinding" &&
res.OrgId().Kind != "RoleBinding" {
continue
}
objMap := res.Map()
subjects, ok := objMap["subjects"].([]interface{})
if subjects == nil || !ok {
continue
}
for i := range subjects {
subject := subjects[i].(map[string]interface{})
kind, foundK := subject["kind"]
name, foundN := subject["name"]
if !foundK || !foundN || kind.(string) != srvAccount.Kind {
continue
}
// a ServiceAccount named “default” exists in every active namespace
if name.(string) == "default" || saMap[name.(string)] {
subject := subjects[i].(map[string]interface{})
transformers.MutateField(
subject, []string{"namespace"},
true, func(_ interface{}) (interface{}, error) {
return p.Namespace, nil
})
subjects[i] = subject
}
}
objMap["subjects"] = subjects
}
}
func (p *NamespaceTransformerPlugin) updateServiceReference(m resmap.ResMap) {
svc := gvk.Gvk{Version: "v1", Kind: "Service"}
svcMap := map[string]bool{}
for _, id := range m.AllIds() {
if id.Gvk.Equals(svc) {
svcMap[id.Name] = true
}
}
for _, res := range m.Resources() {
if res.OrgId().Kind != "ValidatingWebhookConfiguration" &&
res.OrgId().Kind != "MutatingWebhookConfiguration" {
continue
}
objMap := res.Map()
webhooks, ok := objMap["webhooks"].([]interface{})
if webhooks == nil || !ok {
continue
}
for i := range webhooks {
webhook := webhooks[i].(map[string]interface{})
transformers.MutateField(
webhook, []string{"clientConfig", "service"},
false, func(obj interface{}) (interface{}, error) {
svc := obj.(map[string]interface{})
svcName, foundN := svc["name"]
if foundN && svcMap[svcName.(string)] {
svc["namespace"] = p.Namespace
func (o *NamespaceTransformerPlugin) changeNamespace(
referrer *resource.Resource) func(in interface{}) (interface{}, error) {
return func(in interface{}) (interface{}, error) {
switch in.(type) {
case string:
// will happen when the metadata/namespace
// value is replaced
return o.Namespace, nil
case []interface{}:
l, _ := in.([]interface{})
for idx, item := range l {
switch item.(type) {
case map[string]interface{}:
// Will happen when mutating the subjects
// field of ClusterRoleBinding and RoleBinding
inMap, _ := item.(map[string]interface{})
if _, ok := inMap["name"]; !ok {
continue
}
return svc, nil
})
webhooks[i] = webhook
name, ok := inMap["name"].(string)
if !ok {
continue
}
// The only case we need to force the namespace
// if for the "service account". "default" is
// kind of hardcoded here for right now.
if name != "default" {
continue
}
inMap["namespace"] = o.Namespace
l[idx] = inMap
default:
// nothing to do for right now
}
}
return in, nil
case map[string]interface{}:
// Will happen if the createField=true
// when the namespace is added to the
// object
inMap := in.(map[string]interface{})
if len(inMap) == 0 {
return o.Namespace, nil
} else {
return in, nil
}
default:
return in, nil
}
objMap["webhooks"] = webhooks
}
}

View File

@@ -15,7 +15,7 @@ type PatchStrategicMergeTransformerPlugin struct {
rf *resmap.Factory
loadedPatches []*resource.Resource
Paths []types.PatchStrategicMerge `json:"paths,omitempty" yaml:"paths,omitempty"`
Patches string `json:patches,omitempty" yaml:"patches,omitempty"`
Patches string `json:"patches,omitempty" yaml:"patches,omitempty"`
}
//noinspection GoUnusedGlobalVariable
@@ -35,11 +35,18 @@ func (p *PatchStrategicMergeTransformerPlugin) Config(
return fmt.Errorf("empty file path and empty patch content")
}
if len(p.Paths) != 0 {
res, err := p.rf.RF().SliceFromPatches(ldr, p.Paths)
if err != nil {
return err
for _, onePath := range p.Paths {
res, err := p.rf.RF().SliceFromBytes([]byte(onePath))
if err == nil {
p.loadedPatches = append(p.loadedPatches, res...)
continue
}
res, err = p.rf.RF().SliceFromPatches(ldr, []types.PatchStrategicMerge{onePath})
if err != nil {
return err
}
p.loadedPatches = append(p.loadedPatches, res...)
}
p.loadedPatches = res
}
if p.Patches != "" {
res, err := p.rf.RF().SliceFromBytes([]byte(p.Patches))
@@ -70,6 +77,14 @@ func (p *PatchStrategicMergeTransformerPlugin) Transform(m resmap.ResMap) error
if err != nil {
return err
}
// remove the resource from resmap
// when the patch is to $patch: delete that target
if len(target.Map()) == 0 {
err = m.Remove(target.CurId())
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -87,6 +87,7 @@ func (p *PatchTransformerPlugin) Transform(m resmap.ResMap) error {
if err != nil {
return err
}
return nil
}
if p.Target == nil {

View File

@@ -49,22 +49,40 @@ func (p *PrefixSuffixTransformerPlugin) Config(
}
func (p *PrefixSuffixTransformerPlugin) Transform(m resmap.ResMap) error {
if len(p.Prefix) == 0 && len(p.Suffix) == 0 {
return nil
}
// Even if both the Prefix and Suffix are empty we want
// to proceed with the transformation. This allows to add contextual
// information to the resources (AddNamePrefix and AddNameSuffix).
for _, r := range m.Resources() {
if p.shouldSkip(r.OrgId()) {
// Don't change the actual definition
// of a CRD.
continue
}
id := r.OrgId()
// current default configuration contains
// only one entry: "metadata/name" with no GVK
for _, path := range p.FieldSpecs {
if !id.IsSelected(&path.Gvk) {
// With the currrent default configuration,
// because no Gvk is specified, so a wild
// card
continue
}
if smellsLikeANameChange(&path) {
// "metadata/name" is the only field.
// this will add a prefix and a suffix
// to the resource even if those are
// empty
r.AddNamePrefix(p.Prefix)
r.AddNameSuffix(p.Suffix)
}
// the addPrefixSuffix method will not
// change the name if both the prefix and suffix
// are empty.
err := transformers.MutateField(
r.Map(),
path.PathSlice(),

View File

@@ -34,8 +34,15 @@ func (p *ReplicaCountTransformerPlugin) Config(
}
func (p *ReplicaCountTransformerPlugin) Transform(m resmap.ResMap) error {
found := false
for i, replicaSpec := range p.FieldSpecs {
for _, res := range m.GetMatchingResourcesByOriginalId(p.createMatcher(i)) {
matcher := p.createMatcher(i)
matchOriginal := m.GetMatchingResourcesByOriginalId(matcher)
matchCurrent := m.GetMatchingResourcesByCurrentId(matcher)
for _, res := range append(matchOriginal, matchCurrent...) {
found = true
err := transformers.MutateField(
res.Map(), replicaSpec.PathSlice(),
replicaSpec.CreateIfNotPresent, p.addReplicas)
@@ -45,6 +52,15 @@ func (p *ReplicaCountTransformerPlugin) Transform(m resmap.ResMap) error {
}
}
if !found {
gvks := make([]string, len(p.FieldSpecs))
for i, replicaSpec := range p.FieldSpecs {
gvks[i] = replicaSpec.Gvk.String()
}
return fmt.Errorf("Resource with name %s does not match a config with the following GVK %v",
p.Replica.Name, gvks)
}
return nil
}

View File

@@ -5,7 +5,6 @@
package main
import (
"sigs.k8s.io/kustomize/v3/pkg/gvk"
"sigs.k8s.io/kustomize/v3/pkg/ifc"
"sigs.k8s.io/kustomize/v3/pkg/resid"
"sigs.k8s.io/kustomize/v3/pkg/resmap"
@@ -37,23 +36,23 @@ func (p *plugin) Transform(m resmap.ResMap) error {
return nil
}
for _, r := range m.Resources() {
id := r.OrgId()
fs, ok := p.isSelected(id)
if !ok {
continue
}
if len(r.Map()) == 0 {
// Don't mutate empty objects?
continue
}
if doIt(id, fs) {
if err := p.changeNamespace(r, fs); err != nil {
id := r.OrgId()
applicableFs := p.applicableFieldSpecs(id)
for _, fs := range applicableFs {
err := transformers.MutateField(
r.Map(), fs.PathSlice(), fs.CreateIfNotPresent,
p.changeNamespace(r))
if err != nil {
return err
}
}
}
p.updateClusterRoleBinding(m)
p.updateServiceReference(m)
return nil
}
@@ -63,104 +62,64 @@ const metaNamespace = "metadata/namespace"
// all objects have it, even "ClusterKind" objects
// that don't exist in a namespace (the Namespace
// object itself doesn't live in a namespace).
func doIt(id resid.ResId, fs *config.FieldSpec) bool {
return fs.Path != metaNamespace ||
(fs.Path == metaNamespace && id.IsNamespaceableKind())
}
func (p *plugin) changeNamespace(
r *resource.Resource, fs *config.FieldSpec) error {
return transformers.MutateField(
r.Map(), fs.PathSlice(), fs.CreateIfNotPresent,
func(_ interface{}) (interface{}, error) {
return p.Namespace, nil
})
}
func (p *plugin) isSelected(
id resid.ResId) (*config.FieldSpec, bool) {
func (p *plugin) applicableFieldSpecs(id resid.ResId) []config.FieldSpec {
res := []config.FieldSpec{}
for _, fs := range p.FieldSpecs {
if id.IsSelected(&fs.Gvk) {
return &fs, true
if id.IsSelected(&fs.Gvk) && (fs.Path != metaNamespace || (fs.Path == metaNamespace && id.IsNamespaceableKind())) {
res = append(res, fs)
}
}
return nil, false
return res
}
func (p *plugin) updateClusterRoleBinding(m resmap.ResMap) {
srvAccount := gvk.Gvk{Version: "v1", Kind: "ServiceAccount"}
saMap := map[string]bool{}
for _, id := range m.AllIds() {
if id.Gvk.Equals(srvAccount) {
saMap[id.Name] = true
}
}
for _, res := range m.Resources() {
if res.OrgId().Kind != "ClusterRoleBinding" &&
res.OrgId().Kind != "RoleBinding" {
continue
}
objMap := res.Map()
subjects, ok := objMap["subjects"].([]interface{})
if subjects == nil || !ok {
continue
}
for i := range subjects {
subject := subjects[i].(map[string]interface{})
kind, foundK := subject["kind"]
name, foundN := subject["name"]
if !foundK || !foundN || kind.(string) != srvAccount.Kind {
continue
}
// a ServiceAccount named “default” exists in every active namespace
if name.(string) == "default" || saMap[name.(string)] {
subject := subjects[i].(map[string]interface{})
transformers.MutateField(
subject, []string{"namespace"},
true, func(_ interface{}) (interface{}, error) {
return p.Namespace, nil
})
subjects[i] = subject
}
}
objMap["subjects"] = subjects
}
}
func (p *plugin) updateServiceReference(m resmap.ResMap) {
svc := gvk.Gvk{Version: "v1", Kind: "Service"}
svcMap := map[string]bool{}
for _, id := range m.AllIds() {
if id.Gvk.Equals(svc) {
svcMap[id.Name] = true
}
}
for _, res := range m.Resources() {
if res.OrgId().Kind != "ValidatingWebhookConfiguration" &&
res.OrgId().Kind != "MutatingWebhookConfiguration" {
continue
}
objMap := res.Map()
webhooks, ok := objMap["webhooks"].([]interface{})
if webhooks == nil || !ok {
continue
}
for i := range webhooks {
webhook := webhooks[i].(map[string]interface{})
transformers.MutateField(
webhook, []string{"clientConfig", "service"},
false, func(obj interface{}) (interface{}, error) {
svc := obj.(map[string]interface{})
svcName, foundN := svc["name"]
if foundN && svcMap[svcName.(string)] {
svc["namespace"] = p.Namespace
func (o *plugin) changeNamespace(
referrer *resource.Resource) func(in interface{}) (interface{}, error) {
return func(in interface{}) (interface{}, error) {
switch in.(type) {
case string:
// will happen when the metadata/namespace
// value is replaced
return o.Namespace, nil
case []interface{}:
l, _ := in.([]interface{})
for idx, item := range l {
switch item.(type) {
case map[string]interface{}:
// Will happen when mutating the subjects
// field of ClusterRoleBinding and RoleBinding
inMap, _ := item.(map[string]interface{})
if _, ok := inMap["name"]; !ok {
continue
}
return svc, nil
})
webhooks[i] = webhook
name, ok := inMap["name"].(string)
if !ok {
continue
}
// The only case we need to force the namespace
// if for the "service account". "default" is
// kind of hardcoded here for right now.
if name != "default" {
continue
}
inMap["namespace"] = o.Namespace
l[idx] = inMap
default:
// nothing to do for right now
}
}
return in, nil
case map[string]interface{}:
// Will happen if the createField=true
// when the namespace is added to the
// object
inMap := in.(map[string]interface{})
if len(inMap) == 0 {
return o.Namespace, nil
} else {
return in, nil
}
default:
return in, nil
}
objMap["webhooks"] = webhooks
}
}

View File

@@ -28,6 +28,12 @@ metadata:
fieldSpecs:
- path: metadata/namespace
create: true
- path: subjects
kind: RoleBinding
group: rbac.authorization.k8s.io
- path: subjects
kind: ClusterRoleBinding
group: rbac.authorization.k8s.io
`, `
apiVersion: v1
kind: ConfigMap
@@ -54,7 +60,7 @@ apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: system
namespace: test
---
apiVersion: v1
kind: ServiceAccount
@@ -99,6 +105,15 @@ metadata:
name: crd
`)
// Import note: The namespace transformer is in charge of
// the metadata.namespace field. The namespace transformer SHOULD
// NOT modify neither the "namespace" subfield within the
// ClusterRoleBinding.subjects field nor the "namespace"
// subfield in the ValidatingWebhookConfiguration.webhooks field.
// This is the role of the namereference Transformer to handle
// object reference changes (prefix/suffix and namespace).
// For use cases involving simultaneous change of name and namespace,
// refer to namespaces tests in pkg/target test suites.
th.AssertActualEqualsExpected(rm, `
apiVersion: v1
kind: ConfigMap
@@ -145,7 +160,7 @@ subjects:
namespace: test
- kind: ServiceAccount
name: service-account
namespace: test
namespace: system
- kind: ServiceAccount
name: another
namespace: random
@@ -158,7 +173,7 @@ webhooks:
- clientConfig:
service:
name: svc1
namespace: test
namespace: system
name: example1
- clientConfig:
service:
@@ -213,6 +228,12 @@ metadata:
fieldSpecs:
- path: metadata/namespace
create: true
- path: subjects
kind: RoleBinding
group: rbac.authorization.k8s.io
- path: subjects
kind: ClusterRoleBinding
group: rbac.authorization.k8s.io
`, noChangeExpected)
th.AssertActualEqualsExpected(rm, noChangeExpected)

View File

@@ -250,7 +250,7 @@ spec:
`)
}
func TestPatchJson6902TransformerWithInline(t *testing.T) {
func TestPatchJson6902TransformerWithInlineJSON(t *testing.T) {
tc := plugins_test.NewEnvForTest(t).Set()
defer tc.Reset()
@@ -290,3 +290,47 @@ spec:
dnsPolicy: ClusterFirst
`)
}
func TestPatchJson6902TransformerWithInlineYAML(t *testing.T) {
tc := plugins_test.NewEnvForTest(t).Set()
defer tc.Reset()
tc.BuildGoPlugin(
"builtin", "", "PatchJson6902Transformer")
th := kusttest_test.NewKustTestPluginHarness(t, "/app")
rm := th.LoadAndRunTransformer(`
apiVersion: builtin
kind: PatchJson6902Transformer
metadata:
name: notImportantHere
target:
group: apps
version: v1
kind: Deployment
name: myDeploy
jsonOp: |-
- op: add
path: /spec/template/spec/dnsPolicy
value: ClusterFirst
`, target)
th.AssertActualEqualsExpected(rm, `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeploy
spec:
replica: 2
template:
metadata:
labels:
old-label: old-value
spec:
containers:
- image: nginx
name: nginx
dnsPolicy: ClusterFirst
`)
}

View File

@@ -18,7 +18,7 @@ type plugin struct {
rf *resmap.Factory
loadedPatches []*resource.Resource
Paths []types.PatchStrategicMerge `json:"paths,omitempty" yaml:"paths,omitempty"`
Patches string `json:patches,omitempty" yaml:"patches,omitempty"`
Patches string `json:"patches,omitempty" yaml:"patches,omitempty"`
}
//noinspection GoUnusedGlobalVariable
@@ -36,11 +36,18 @@ func (p *plugin) Config(
return fmt.Errorf("empty file path and empty patch content")
}
if len(p.Paths) != 0 {
res, err := p.rf.RF().SliceFromPatches(ldr, p.Paths)
if err != nil {
return err
for _, onePath := range p.Paths {
res, err := p.rf.RF().SliceFromBytes([]byte(onePath))
if err == nil {
p.loadedPatches = append(p.loadedPatches, res...)
continue
}
res, err = p.rf.RF().SliceFromPatches(ldr, []types.PatchStrategicMerge{onePath})
if err != nil {
return err
}
p.loadedPatches = append(p.loadedPatches, res...)
}
p.loadedPatches = res
}
if p.Patches != "" {
res, err := p.rf.RF().SliceFromBytes([]byte(p.Patches))
@@ -71,6 +78,14 @@ func (p *plugin) Transform(m resmap.ResMap) error {
if err != nil {
return err
}
// remove the resource from resmap
// when the patch is to $patch: delete that target
if len(target.Map()) == 0 {
err = m.Remove(target.CurId())
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -4,6 +4,7 @@
package main_test
import (
"fmt"
"strings"
"testing"
@@ -77,7 +78,9 @@ paths:
t.Fatalf("expected error")
}
if !strings.Contains(err.Error(),
"cannot read file \"/app/patch.yaml\"") {
"cannot read file \"/app/patch.yaml\"") &&
!strings.Contains(err.Error(),
"cannot unmarshal string") {
t.Fatalf("unexpected err: %v", err)
}
}
@@ -180,7 +183,7 @@ spec:
`)
}
func TestPatchStrategicMergeTransformerWithInline(t *testing.T) {
func TestPatchStrategicMergeTransformerWithInlineJSON(t *testing.T) {
tc := plugins_test.NewEnvForTest(t).Set()
defer tc.Reset()
@@ -215,6 +218,58 @@ spec:
`)
}
func TestPatchStrategicMergeTransformerWithInlineYAML(t *testing.T) {
tc := plugins_test.NewEnvForTest(t).Set()
defer tc.Reset()
tc.BuildGoPlugin(
"builtin", "", "PatchStrategicMergeTransformer")
th := kusttest_test.NewKustTestPluginHarness(t, "/app")
rm := th.LoadAndRunTransformer(`
apiVersion: builtin
kind: PatchStrategicMergeTransformer
metadata:
name: notImportantHere
patches: |-
apiVersion: apps/v1
metadata:
name: myDeploy
kind: Deployment
spec:
replica: 3
---
apiVersion: apps/v1
metadata:
name: myDeploy
kind: Deployment
spec:
template:
spec:
containers:
- name: nginx
image: nginx:latest
`, target)
th.AssertActualEqualsExpected(rm, `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeploy
spec:
replica: 3
template:
metadata:
labels:
old-label: old-value
spec:
containers:
- image: nginx:latest
name: nginx
`)
}
func TestPatchStrategicMergeTransformerMultiplePatches(t *testing.T) {
tc := plugins_test.NewEnvForTest(t).Set()
defer tc.Reset()
@@ -533,3 +588,696 @@ paths:
t.Fatalf("expected error to contain %q but get %v", "conflict", err)
}
}
// simple utility function to add an namespace in a resource
// used as base, patch or expected result. Simply looks
// for specs: in order to add namespace: xxxx before this line
func addNamespace(namespace string, base string) string {
res := strings.Replace(base,
"\nspec:\n",
"\n namespace: "+namespace+"\nspec:\n",
1)
return res
}
// unExpectedError function handles unexpected error
func unExpectedError(t *testing.T, name string, err error) {
t.Fatalf("%q; - unexpected error %v", name, err)
}
// compareExpectedError compares the expectedError and the actualError return by GetFieldValue
func compareExpectedError(t *testing.T, name string, err error, errorMsg string) {
if err == nil {
t.Fatalf("%q; - should return error, but no error returned", name)
}
if !strings.Contains(err.Error(), errorMsg) {
t.Fatalf("%q; - expected error: \"%s\", got error: \"%v\"",
name, errorMsg, err.Error())
}
}
const Deployment string = "Deployment"
const MyCRD string = "MyCRD"
// baseResource produces a base object which used to test
// patch transformation
// Also the structure is matching the Deployment syntax
// the kind can be replaced to allow testing using CRD
// without access to the schema
func baseResource(kind string) string {
res := `
apiVersion: apps/v1
kind: %s
metadata:
name: deploy1
spec:
template:
metadata:
labels:
old-label: old-value
spec:
containers:
- name: nginx
image: nginx`
return fmt.Sprintf(res, kind)
}
// addContainerAndEnvPatch produces a patch object which adds
// an entry in the env slice of the first/nginx container
// as well as adding a label in the metadata
// Note that for SMP/WithSchema merge, the name:nginx entry
// is mandatory
func addLabelAndEnvPatch(kind string) string {
res := `
apiVersion: apps/v1
kind: %s
metadata:
name: deploy1
spec:
template:
metadata:
labels:
some-label: some-value
spec:
containers:
- name: nginx
env:
- name: SOMEENV
value: SOMEVALUE`
return fmt.Sprintf(res, kind)
}
// addContainerAndEnvPatch produces a patch object which adds
// an entry in the env slice of the first/nginx container
// as well as adding a second container in the container list
// Note that for SMP/WithSchema merge, the name:nginx entry
// is mandatory
func addContainerAndEnvPatch(kind string) string {
res := `
apiVersion: apps/v1
kind: %s
metadata:
name: deploy1
spec:
template:
spec:
containers:
- name: nginx
env:
- name: ANOTHERENV
value: ANOTHERVALUE
- name: anothercontainer
image: anotherimage`
return fmt.Sprintf(res, kind)
}
// addContainerAndEnvPatch produces a patch object which replaces
// the value of the image field in the first/nginx container
// Note that for SMP/WithSchema merge, the name:nginx entry
// is mandatory
func changeImagePatch(kind string, newImage string) string {
res := `
apiVersion: apps/v1
kind: %s
metadata:
name: deploy1
spec:
template:
spec:
containers:
- name: nginx
image: %s`
return fmt.Sprintf(res, kind, newImage)
}
// utility method building the expected output of a SMP
func expectedResultSMP() string {
return `apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy1
spec:
template:
metadata:
labels:
old-label: old-value
some-label: some-value
spec:
containers:
- env:
- name: SOMEENV
value: SOMEVALUE
image: nginx
name: nginx
`
}
// utility method building the expected output of a JMP.
// imagename parameter allows to build a result consistent
// with the JMP behavior which basically overrides the
// entire "containers" list.
func expectedResultJMP(imagename string) string {
res := `apiVersion: apps/v1
kind: MyCRD
metadata:
name: deploy1
spec:
template:
metadata:
labels:
old-label: old-value
some-label: some-value
spec:
containers:
- env:
- name: SOMEENV
value: SOMEVALUE
name: nginx
`
if imagename == "" {
return res
}
res = `apiVersion: apps/v1
kind: MyCRD
metadata:
name: deploy1
spec:
template:
metadata:
labels:
old-label: old-value
some-label: some-value
spec:
containers:
- image: %s
name: nginx
`
return fmt.Sprintf(res, imagename)
}
// utility method to build the expected result of a multipatch
// the order of the patches still have influence especially
// in the insertion location within arrays.
func expectedResultMultiPatch(kind string, reversed bool) string {
res := `apiVersion: apps/v1
kind: %s
metadata:
name: deploy1
spec:
template:
metadata:
labels:
old-label: old-value
some-label: some-value
spec:
containers:
- env:
- name: ANOTHERENV
value: ANOTHERVALUE
- name: SOMEENV
value: SOMEVALUE
image: nginx:latest
name: nginx
- image: anotherimage
name: anothercontainer
`
reversedres := `apiVersion: apps/v1
kind: %s
metadata:
name: deploy1
spec:
template:
metadata:
labels:
old-label: old-value
some-label: some-value
spec:
containers:
- env:
- name: SOMEENV
value: SOMEVALUE
- name: ANOTHERENV
value: ANOTHERVALUE
image: nginx:latest
name: nginx
- image: anotherimage
name: anothercontainer
`
if reversed {
return fmt.Sprintf(reversedres, kind)
}
return fmt.Sprintf(res, kind)
}
func toConfig(patches ...string) string {
config := `
apiVersion: builtin
kind: PatchStrategicMergeTransformer
metadata:
name: notImportantHere
paths:
`
for idx, _ := range patches {
config = fmt.Sprintf("%s\n- ./patch%d.yaml", config, idx)
}
return config
}
// TestSinglePatch validates the single patch use cases
// regarless of the schema availibility, which in turns
// relies on StrategicMergePatch or simple JSON Patch.
func TestSinglePatch(t *testing.T) {
tests := []struct {
name string
base string
patch string
expected string
errorExpected bool
errorMsg string
}{
{
name: "withschema",
base: baseResource(Deployment),
patch: addLabelAndEnvPatch(Deployment),
errorExpected: false,
expected: expectedResultSMP(),
},
{
name: "noschema",
base: baseResource(MyCRD),
patch: addLabelAndEnvPatch(MyCRD),
errorExpected: false,
expected: expectedResultJMP(""),
},
}
tc := plugins_test.NewEnvForTest(t).Set()
defer tc.Reset()
tc.BuildGoPlugin(
"builtin", "", "PatchStrategicMergeTransformer")
for _, test := range tests {
th := kusttest_test.NewKustTestPluginHarness(t, fmt.Sprintf("/%s", test.name))
th.WriteF(fmt.Sprintf("/%s/patch%d.yaml", test.name, 0), test.patch)
if test.errorExpected {
err := th.ErrorFromLoadAndRunTransformer(toConfig(test.patch), test.base)
compareExpectedError(t, test.name, err, test.errorMsg)
} else {
rm := th.LoadAndRunTransformer(toConfig(test.patch), test.base)
th.AssertActualEqualsExpected(rm, test.expected)
}
}
}
// TestMultiplePatches checks that the patches are applied
// properly, that the same result is obtained,
// regardless of the order of the patches and regardless
// of the schema availibility (SMP vs JSON)
func TestMultiplePatches(t *testing.T) {
tests := []struct {
name string
base string
patch []string
expected string
errorExpected bool
errorMsg string
}{
{
name: "withschema-label-image-container",
base: baseResource(Deployment),
patch: []string{
addLabelAndEnvPatch(Deployment),
changeImagePatch(Deployment, "nginx:latest"),
addContainerAndEnvPatch(Deployment),
},
errorExpected: false,
expected: expectedResultMultiPatch(Deployment, false),
},
{
name: "withschema-image-container-label",
base: baseResource(Deployment),
patch: []string{
changeImagePatch(Deployment, "nginx:latest"),
addContainerAndEnvPatch(Deployment),
addLabelAndEnvPatch(Deployment),
},
errorExpected: false,
expected: expectedResultMultiPatch(Deployment, true),
},
{
name: "withschema-container-label-image",
base: baseResource(Deployment),
patch: []string{
addContainerAndEnvPatch(Deployment),
addLabelAndEnvPatch(Deployment),
changeImagePatch(Deployment, "nginx:latest"),
},
errorExpected: false,
expected: expectedResultMultiPatch(Deployment, true),
},
{
name: "noschema-label-image-container",
base: baseResource(MyCRD),
patch: []string{
addLabelAndEnvPatch(MyCRD),
changeImagePatch(MyCRD, "nginx:latest"),
addContainerAndEnvPatch(MyCRD),
},
// This should work
errorExpected: true,
errorMsg: "conflict",
},
{
name: "noschema-image-container-label",
base: baseResource(MyCRD),
patch: []string{
changeImagePatch(MyCRD, "nginx:latest"),
addContainerAndEnvPatch(MyCRD),
addLabelAndEnvPatch(MyCRD),
},
// This should work
errorExpected: true,
errorMsg: "conflict",
},
{
name: "noschema-container-label-image",
base: baseResource(MyCRD),
patch: []string{
addContainerAndEnvPatch(MyCRD),
addLabelAndEnvPatch(MyCRD),
changeImagePatch(MyCRD, "nginx:latest"),
},
// This should work
errorExpected: true,
errorMsg: "conflict",
},
}
tc := plugins_test.NewEnvForTest(t).Set()
defer tc.Reset()
tc.BuildGoPlugin(
"builtin", "", "PatchStrategicMergeTransformer")
for _, test := range tests {
th := kusttest_test.NewKustTestPluginHarness(t, fmt.Sprintf("/%s", test.name))
for idx, patch := range test.patch {
th.WriteF(fmt.Sprintf("/%s/patch%d.yaml", test.name, idx), patch)
}
if test.errorExpected {
err := th.ErrorFromLoadAndRunTransformer(toConfig(test.patch...), test.base)
compareExpectedError(t, test.name, err, test.errorMsg)
} else {
rm := th.LoadAndRunTransformer(toConfig(test.patch...), test.base)
th.AssertActualEqualsExpected(rm, test.expected)
}
}
}
// TestMultiplePatchesWithConflict checks that the conflict are
// detected regardless of the order of the patches and regardless
// of the schema availibility (SMP vs JSON)
func TestMultiplePatchesWithConflict(t *testing.T) {
tests := []struct {
name string
base string
patch []string
expected string
errorExpected bool
errorMsg string
}{
{
name: "withschema-label-latest-1.7.9",
base: baseResource(Deployment),
patch: []string{
addLabelAndEnvPatch(Deployment),
changeImagePatch(Deployment, "nginx:latest"),
changeImagePatch(Deployment, "nginx:1.7.9"),
},
errorExpected: true,
errorMsg: "conflict",
},
{
name: "withschema-latest-label-1.7.9",
base: baseResource(Deployment),
patch: []string{
changeImagePatch(Deployment, "nginx:latest"),
addLabelAndEnvPatch(Deployment),
changeImagePatch(Deployment, "nginx:1.7.9"),
},
errorExpected: true,
errorMsg: "conflict",
},
{
name: "withschema-1.7.9-label-latest",
base: baseResource(Deployment),
patch: []string{
changeImagePatch(Deployment, "nginx:1.7.9"),
addLabelAndEnvPatch(Deployment),
changeImagePatch(Deployment, "nginx:latest"),
},
errorExpected: true,
errorMsg: "conflict",
},
{
name: "withschema-1.7.9-latest-label",
base: baseResource(Deployment),
patch: []string{
changeImagePatch(Deployment, "nginx:1.7.9"),
changeImagePatch(Deployment, "nginx:latest"),
addLabelAndEnvPatch(Deployment),
changeImagePatch(Deployment, "nginx:nginx"),
},
errorExpected: true,
errorMsg: "conflict",
},
{
name: "noschema-label-latest-1.7.9",
base: baseResource(MyCRD),
patch: []string{
addLabelAndEnvPatch(MyCRD),
changeImagePatch(MyCRD, "nginx:latest"),
changeImagePatch(MyCRD, "nginx:1.7.9"),
},
errorExpected: true,
errorMsg: "conflict",
},
{
name: "noschema-latest-label-1.7.9",
base: baseResource(MyCRD),
patch: []string{
changeImagePatch(MyCRD, "nginx:latest"),
addLabelAndEnvPatch(MyCRD),
changeImagePatch(MyCRD, "nginx:1.7.9"),
},
errorExpected: false,
// There is no conflict detected. It should
// be but the JMPConflictDector ignores it.
// See https://github.com/kubernetes-sigs/kustomize/issues/1370
expected: expectedResultJMP("nginx:1.7.9"),
},
{
name: "noschema-1.7.9-label-latest",
base: baseResource(MyCRD),
patch: []string{
changeImagePatch(MyCRD, "nginx:1.7.9"),
addLabelAndEnvPatch(MyCRD),
changeImagePatch(MyCRD, "nginx:latest"),
},
errorExpected: false,
// There is no conflict detected. It should
// be but the JMPConflictDector ignores it.
// See https://github.com/kubernetes-sigs/kustomize/issues/1370
expected: expectedResultJMP("nginx:latest"),
},
{
name: "noschema-1.7.9-latest-label",
base: baseResource(MyCRD),
patch: []string{
changeImagePatch(MyCRD, "nginx:1.7.9"),
changeImagePatch(MyCRD, "nginx:latest"),
addLabelAndEnvPatch(MyCRD),
changeImagePatch(MyCRD, "nginx:nginx"),
},
errorExpected: true,
},
}
tc := plugins_test.NewEnvForTest(t).Set()
defer tc.Reset()
tc.BuildGoPlugin(
"builtin", "", "PatchStrategicMergeTransformer")
for _, test := range tests {
th := kusttest_test.NewKustTestPluginHarness(t, fmt.Sprintf("/%s", test.name))
for idx, patch := range test.patch {
th.WriteF(fmt.Sprintf("/%s/patch%d.yaml", test.name, idx), patch)
}
if test.errorExpected {
err := th.ErrorFromLoadAndRunTransformer(toConfig(test.patch...), test.base)
compareExpectedError(t, test.name, err, test.errorMsg)
} else {
rm := th.LoadAndRunTransformer(toConfig(test.patch...), test.base)
th.AssertActualEqualsExpected(rm, test.expected)
}
}
}
// TestMultipleNamespaces before the same patch
// on two objects have the same name but in a different namespaces
func TestMultipleNamespaces(t *testing.T) {
tests := []struct {
name string
base []string
patch []string
expected []string
errorExpected bool
errorMsg string
}{
{
name: "withschema-ns1-ns2",
base: []string{
addNamespace("ns1", baseResource(Deployment)),
addNamespace("ns2", baseResource(Deployment)),
},
patch: []string{
addNamespace("ns1", addLabelAndEnvPatch(Deployment)),
addNamespace("ns2", addLabelAndEnvPatch(Deployment)),
},
errorExpected: false,
expected: []string{
addNamespace("ns1", expectedResultSMP()),
addNamespace("ns2", expectedResultSMP()),
},
},
{
name: "noschema-ns1-ns2",
base: []string{
addNamespace("ns1", baseResource(MyCRD)),
addNamespace("ns2", baseResource(MyCRD)),
},
patch: []string{
addNamespace("ns1", addLabelAndEnvPatch(MyCRD)),
addNamespace("ns2", addLabelAndEnvPatch(MyCRD)),
},
errorExpected: false,
expected: []string{
addNamespace("ns1", expectedResultJMP("")),
addNamespace("ns2", expectedResultJMP("")),
},
},
{
name: "withschema-ns1-ns2",
base: []string{addNamespace("ns1", baseResource(Deployment))},
patch: []string{addNamespace("ns2", changeImagePatch(Deployment, "nginx:1.7.9"))},
errorExpected: true,
errorMsg: "failed to find unique target for patch",
},
{
name: "withschema-nil-ns2",
base: []string{baseResource(Deployment)},
patch: []string{addNamespace("ns2", changeImagePatch(Deployment, "nginx:1.7.9"))},
errorExpected: true,
errorMsg: "failed to find unique target for patch",
},
{
name: "withschema-ns1-nil",
base: []string{addNamespace("ns1", baseResource(Deployment))},
patch: []string{changeImagePatch(Deployment, "nginx:1.7.9")},
errorExpected: true,
errorMsg: "failed to find unique target for patch",
},
{
name: "noschema-ns1-ns2",
base: []string{addNamespace("ns1", baseResource(MyCRD))},
patch: []string{addNamespace("ns2", changeImagePatch(MyCRD, "nginx:1.7.9"))},
errorExpected: true,
errorMsg: "failed to find unique target for patch",
},
{
name: "noschema-nil-ns2",
base: []string{baseResource(MyCRD)},
patch: []string{addNamespace("ns2", changeImagePatch(MyCRD, "nginx:1.7.9"))},
errorExpected: true,
errorMsg: "failed to find unique target for patch",
},
{
name: "noschema-ns1-nil",
base: []string{addNamespace("ns1", baseResource(MyCRD))},
patch: []string{changeImagePatch(MyCRD, "nginx:1.7.9")},
errorExpected: true,
errorMsg: "failed to find unique target for patch",
},
}
tc := plugins_test.NewEnvForTest(t).Set()
defer tc.Reset()
tc.BuildGoPlugin(
"builtin", "", "PatchStrategicMergeTransformer")
for _, test := range tests {
th := kusttest_test.NewKustTestPluginHarness(t, fmt.Sprintf("/%s", test.name))
for idx, patch := range test.patch {
th.WriteF(fmt.Sprintf("/%s/patch%d.yaml", test.name, idx), patch)
}
if test.errorExpected {
err := th.ErrorFromLoadAndRunTransformer(toConfig(test.patch...), strings.Join(test.base, "\n---\n"))
compareExpectedError(t, test.name, err, test.errorMsg)
} else {
rm := th.LoadAndRunTransformer(toConfig(test.patch...), strings.Join(test.base, "\n---\n"))
th.AssertActualEqualsExpected(rm, strings.Join(test.expected, "---\n"))
}
}
}
func TestPatchStrategicMergeTransformerPatchDelete(t *testing.T) {
tc := plugins_test.NewEnvForTest(t).Set()
defer tc.Reset()
tc.BuildGoPlugin(
"builtin", "", "PatchStrategicMergeTransformer")
th := kusttest_test.NewKustTestPluginHarness(t, "/app")
th.WriteF("/app/patch.yaml", `
apiVersion: apps/v1
metadata:
name: myDeploy
kind: Deployment
$patch: delete
`)
rm := th.LoadAndRunTransformer(`
apiVersion: builtin
kind: PatchStrategicMergeTransformer
metadata:
name: notImportantHere
paths:
- patch.yaml
`, target)
th.AssertActualEqualsExpected(rm, ``)
}

View File

@@ -88,6 +88,7 @@ func (p *plugin) Transform(m resmap.ResMap) error {
if err != nil {
return err
}
return nil
}
if p.Target == nil {

View File

@@ -50,22 +50,40 @@ func (p *plugin) Config(
}
func (p *plugin) Transform(m resmap.ResMap) error {
if len(p.Prefix) == 0 && len(p.Suffix) == 0 {
return nil
}
// Even if both the Prefix and Suffix are empty we want
// to proceed with the transformation. This allows to add contextual
// information to the resources (AddNamePrefix and AddNameSuffix).
for _, r := range m.Resources() {
if p.shouldSkip(r.OrgId()) {
// Don't change the actual definition
// of a CRD.
continue
}
id := r.OrgId()
// current default configuration contains
// only one entry: "metadata/name" with no GVK
for _, path := range p.FieldSpecs {
if !id.IsSelected(&path.Gvk) {
// With the currrent default configuration,
// because no Gvk is specified, so a wild
// card
continue
}
if smellsLikeANameChange(&path) {
// "metadata/name" is the only field.
// this will add a prefix and a suffix
// to the resource even if those are
// empty
r.AddNamePrefix(p.Prefix)
r.AddNameSuffix(p.Suffix)
}
// the addPrefixSuffix method will not
// change the name if both the prefix and suffix
// are empty.
err := transformers.MutateField(
r.Map(),
path.PathSlice(),

View File

@@ -35,8 +35,15 @@ func (p *plugin) Config(
}
func (p *plugin) Transform(m resmap.ResMap) error {
found := false
for i, replicaSpec := range p.FieldSpecs {
for _, res := range m.GetMatchingResourcesByOriginalId(p.createMatcher(i)) {
matcher := p.createMatcher(i)
matchOriginal := m.GetMatchingResourcesByOriginalId(matcher)
matchCurrent := m.GetMatchingResourcesByCurrentId(matcher)
for _, res := range append(matchOriginal, matchCurrent...) {
found = true
err := transformers.MutateField(
res.Map(), replicaSpec.PathSlice(),
replicaSpec.CreateIfNotPresent, p.addReplicas)
@@ -46,6 +53,15 @@ func (p *plugin) Transform(m resmap.ResMap) error {
}
}
if !found {
gvks := make([]string, len(p.FieldSpecs))
for i, replicaSpec := range p.FieldSpecs {
gvks[i] = replicaSpec.Gvk.String()
}
return fmt.Errorf("Resource with name %s does not match a config with the following GVK %v",
p.Replica.Name, gvks)
}
return nil
}

View File

@@ -151,3 +151,85 @@ spec:
app: app
`)
}
func TestMatchesCurrentID(t *testing.T) {
tc := plugins_test.NewEnvForTest(t).Set()
defer tc.Reset()
tc.BuildGoPlugin("builtin", "", "PrefixSuffixTransformer")
tc.BuildGoPlugin("builtin", "", "ReplicaCountTransformer")
th := kusttest_test.NewKustTestPluginHarness(t, "/app")
rm := th.LoadAndRunTransformer(`
apiVersion: builtin
kind: PrefixSuffixTransformer
metadata:
name: notImportantHere
suffix: -test
fieldSpecs:
- path: metadata/name
`, `
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment`)
rm, _ = th.RunTransformerFromResMap(`
apiVersion: builtin
kind: ReplicaCountTransformer
metadata:
name: notImportantHere
replica:
name: deployment-test
count: 23
fieldSpecs:
- path: spec/replicas
create: true
kind: Deployment`, rm)
th.AssertActualEqualsExpected(rm, `
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-test
spec:
replicas: 23
`)
}
func TestNoMatch(t *testing.T) {
tc := plugins_test.NewEnvForTest(t).Set()
defer tc.Reset()
tc.BuildGoPlugin("builtin", "", "ReplicaCountTransformer")
th := kusttest_test.NewKustTestPluginHarness(t, "/app")
err := th.ErrorFromLoadAndRunTransformer(`
apiVersion: builtin
kind: ReplicaCountTransformer
metadata:
name: notImportantHere
replica:
name: service
count: 3
fieldSpecs:
- path: spec/replicas
create: true
kind: Deployment
`, `
kind: Service
metadata:
name: service
spec:
`)
if err == nil {
t.Fatalf("No match should return an error")
}
if err.Error() !=
"Resource with name service does not match a config with the following GVK [~G_~V_Deployment]" {
t.Fatalf("Unexpected error: %v", err)
}
}