Compare commits

..

1 Commits

Author SHA1 Message Date
Jeff Regan
f59f12947b Update release instructions. 2020-08-28 11:39:17 -07:00
1425 changed files with 97335 additions and 264483 deletions

View File

@@ -1,8 +0,0 @@
.github
docs
examples
functions
hack
site
travis
*.md

View File

@@ -1,68 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ""
labels:
- kind/bug
assignees: ""
---
<!--
Please read this page: https://kubernetes-sigs.github.io/kustomize/contributing/bugs/ before
filing a bug
-->
<!-- Feel free to skip the sections if they are not applicable. -->
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**Files that can reproduce the issue**
<!--
We cannot figure out or fix the issue if we don't know how to reproduce. Please
provide a minimum set of files that can reproduce the issue. You can paste the
file contents here or provide a link to a tarball or git repo.
Example:
kustomization.yaml
```
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
...
```
resources.yaml
```
apiVersion: v1
kind: Deployment
...
```
...
-->
**Expected output**
<!-- What's the expected output? -->
**Actual output**
<!-- What's the actual output? -->
**Kustomize version**
<!-- Please use the latest version when it's possible. -->
**Platform**
<!-- Linux/macOS/Windows? -->
**Additional context**
<!-- Add any other context about the problem here. -->

View File

@@ -1 +0,0 @@
blank_issues_enabled: true

View File

@@ -1,26 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ""
labels:
- kind/feature
assignees: ""
---
<!-- Feel free to skip the sections if they are not applicable. -->
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->

View File

@@ -1,9 +0,0 @@
---
name: Question
about: Ask a question about the kustomize
title: "[Question]"
labels: ""
assignees: ""
---
<!-- Please describe your question here -->

View File

@@ -16,14 +16,14 @@ jobs:
- name: Set up Go 1.x - name: Set up Go 1.x
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: ^1.16 go-version: ^1.13
id: go id: go
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Lint - name: Lint
run: ./hack/kyaml-pre-commit.sh run: ./travis/kyaml-pre-commit.sh
env: env:
KUSTOMIZE_DOCKER_E2E: false # don't need to do e2e tests for linting KUSTOMIZE_DOCKER_E2E: false # don't need to do e2e tests for linting
@@ -35,7 +35,7 @@ jobs:
- name: Set up Go 1.x - name: Set up Go 1.x
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: ^1.16 go-version: ^1.13
id: go id: go
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
@@ -45,10 +45,6 @@ jobs:
run: go test -cover ./... run: go test -cover ./...
working-directory: ./kyaml working-directory: ./kyaml
- name: Test api
run: go test -cover ./... -ldflags "-X sigs.k8s.io/kustomize/api/provenance.version=v444.333.222"
working-directory: ./api
- name: Test cmd/config - name: Test cmd/config
run: go test -cover ./... run: go test -cover ./...
working-directory: ./cmd/config working-directory: ./cmd/config
@@ -63,7 +59,7 @@ jobs:
- name: Set up Go 1.x - name: Set up Go 1.x
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: ^1.16 go-version: ^1.13
id: go id: go
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
@@ -73,10 +69,6 @@ jobs:
run: go test -cover ./... run: go test -cover ./...
working-directory: ./kyaml working-directory: ./kyaml
- name: Test api
run: go test -cover ./... -ldflags "-X sigs.k8s.io/kustomize/api/provenance.version=v444.333.222"
working-directory: ./api
- name: Test cmd/config - name: Test cmd/config
run: go test -cover ./... run: go test -cover ./...
working-directory: ./cmd/config working-directory: ./cmd/config
@@ -91,7 +83,7 @@ jobs:
- name: Set up Go 1.x - name: Set up Go 1.x
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: ^1.16 go-version: ^1.13
id: go id: go
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
@@ -101,11 +93,6 @@ jobs:
run: go test -cover ./... run: go test -cover ./...
working-directory: ./kyaml working-directory: ./kyaml
# TODO: uncomment once Windows tests are passing.
# - name: Test api
# run: go test -cover ./... -ldflags "-X sigs.k8s.io/kustomize/api/provenance.version=v444.333.222"
# working-directory: ./api
- name: Test cmd/config - name: Test cmd/config
run: go test -cover ./... run: go test -cover ./...
working-directory: ./cmd/config working-directory: ./cmd/config

View File

@@ -1,299 +0,0 @@
# Architecture
* _Updated: December 2021_
This document describes the repository organization and the kustomize
build process. It's meant to lower the barrier to learning and
contributing to the code base.
If not kept up to date, it will just be a historical snapshot.
## Repository layout
[human-edited docs]: https://github.com/kubernetes-sigs/cli-experimental/tree/master/site
[generated docs]: https://github.com/kubernetes-sigs/cli-experimental/tree/master/docs
[rendered docs]: https://kubectl.docs.kubernetes.io
[openapi]: https://kubernetes.io/blog/2016/12/kubernetes-supports-openapi
[`api` module]: https://github.com/kubernetes-sigs/kustomize/blob/master/api/go.mod
[`api`]: #the-api-module
[`cmd/config` module]: https://github.com/kubernetes-sigs/kustomize/blob/master/cmd/config/go.mod
[`cmd/config`]: #the-cmdconfig-module
[`kustomize` module]: https://github.com/kubernetes-sigs/kustomize/blob/master/kustomize/go.mod
[`kustomize`]: #the-kustomize-module
[`kyaml` module]: https://github.com/kubernetes-sigs/kustomize/blob/master/kyaml/go.mod
[`kyaml`]: #the-kyaml-module
[`kyaml/kio.Filter`]: https://github.com/Kubernetes-sigs/kustomize/blob/master/kyaml/kio/kio.go
[`go-yaml`]: https://github.com/go-yaml/yaml/tree/v3
[3922]: https://github.com/kubernetes-sigs/kustomize/issues/3922
| directory | purpose |
| ---------: | :---------- |
| `api` | The [`api`] module, holding high level kustomize code, suitable for import by other programs. |
| `cmd` | Various Go programs aiding repo management. See also `hack`. As an outlier, includes the special [`cmd/config`] module. |
| `docs` | Old home of documentation; contains pointers to new homes: [human-edited docs], [generated docs] and [rendered docs]. |
| `examples` | Full kustomization examples that run as pre-merge tests. |
| `functions` | Examples of plugins in KRM function form. TODO([3922]): Move under `plugin`. |
| `hack` | Various shell scripts to help with code management. |
| `kustomize` | The [`kustomize`] module holds the `main.go` for kustomize. |
| `kyaml` | The [`kyaml`] module, holding Kubernetes-specific YAML editing packages used by the [`api`] module. Wraps [`go-yaml`] v3.|
| `plugin` | Examples of Kustomize plugins. |
| `releasing` | Instructions for releasing the various modules. |
| `site` | Old generated documentation, kept to provide redirection links to the new docs. |
## Modules
[semantically versioned]: https://semver.org
[Go modules]: https://github.com/golang/go/wiki/Modules
The [Go modules] in the kustomize repository are [semantically versioned].
### `kustomize`
> _Depends on [`api`], [`cmd/config`], [`kyaml`]_
The [`kustomize` module] contains the `main.go` for `kustomize`, buildable with
```
(cd kustomize; go install .)
```
[appears in kubectl]: https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/kubectl/pkg/cmd/kustomize/kustomize.go
Below this are packages containing
[cobra](http://github.com/spf13/cobra) commands implementing `build`,
`edit`, `fix`, etc., packages linked together by `main.go`.
These command packages are intentionally public, semantically
versioned, and can be used in other programs. Specifically, the
`kustomize build` command [appears in kubectl] as `kubectl kustomize`.
The code in the `build` package is dominated by flag validation,
with minimal business logic. The critical lines are something
like
```
# Make a kustomizer.
k := krusty.MakeKustomizer(
HonorKustomizeFlags(krusty.MakeDefaultOptions()),
)
# Run the kustomizer, sending location of kustomization.yaml
m := k.Run(fSys, "/path/to/dir")
# Write the result as YAML.
writer.Write(m.AsYaml())
```
The `krusty` package is in the [`api`] module.
### `api`
> _Depends on [`kyaml`] and code generated from builtin plugin modules_
The [`api` module] is used by CLI programs like `kustomize` and `kubectl`
to read and honor `kustomization.yaml` files and all that implies.
The main public packages in the [`api` module] are
| package | |
| --------: | :---------- |
| `filters` | Implementations of [`kyaml/kio.Filter`] used by kustomize to transform Kubernetes objects. |
| `konfig` | Configuration methods and constants in the kustomize API. |
| `krusty` | Primary API entry point. Holds the kustomizer and hundreds of tests for it. |
| `loader` | Loads kustomization files and the files they refer to, enforcing security rules. |
| `resmap` | The primary internal data structure over which the kustomizer and filters work. |
| `types` | The `Kustomization` object and ancillary structs. |
### `cmd/config`
> _Depends on [`kyaml`]_
This module contains cobra commands and kyaml-based functionality to
provide unix-like file manipulation commands to kustomize like `grep`
and `tree`. These commands may be included in any program that
manipulates k8s YAML (e.g. kustomize).
### `kyaml`
> _Has no in-repo dependence_
The [`kyaml` module] is a kubernetes-focussed enhancement of [go-yaml].
The YAML manipulation performed by a kustomize is based on these libraries.
These libraries evolve independently of kustomize, and other programs depend on them.
The key public packages in the [`kyaml` module] include
| package | |
| --------: | :---------- |
| `errors` | Wrapper for the go-errors/errors lib |
| `filesys` | A kustomize-specific file system abstraction, to ease writing tests |
| `fn/framework` | An SDK for writing KRM Functions in Go |
| `fn/runtime` | Implements the runtime for KRM Function extensions |
| `kio` | Libraries for reading and writing collections of Kubernetes resources as RNodes |
| `openapi` | Loads and accesses openapi schemas for schema-aware resource manipultaion |
| `resid` | Representations to aid in unique identification of Kubernetes resources |
| `yaml` | A Kubernetes-focused wrapper of [go-yaml], notably including the RNode object |
-------
## How _kustomize build_ works
The command `kustomize build` accepts a single string argument,
which must resolve to a directory, possibly in a git repository,
called the _kustomization root_.
This directory must contain a file called `kustomization.yaml`, with
YAML that marshals into a single instance of a `Kustomization` object.
For the remainder of this document, the word _kustomization_ refers to
either of these things.
This kustomization is the access point to a directed, acyclic graph of
Kubernetes objects, including other kustomizations, to include in a
build.
Execution of `build` starts and ends in the [`api`] module,
frequently dipping into the [`kyaml`] module for lower level
YAML manipulation.
### The `build` flow
- Validate command lines arguments and flags.
- Make a `Kustomizer` as a function of those arguments.
- Call `Run` on the kustomizer, passing it the path to the
kustomization.
`Run` returns an instance of `ResMap`, the `api` package's
representation of a set of kubernetes `Resource` objects.
This structure offers resource lookup methods (map behavior),
but also retains the resources in the order they were
specified in kustomization files (list behavior).
Post-run, the objects are fully hydrated, per the
instructions in the kustomization.
- Marshal the objects as YAML to a file or `stdout`.
### The `Run` function
- Create various objects
- A `ResMap` factory.
Makes `ResMaps` from byte streams, other `ResMaps`, etc.
- A file `loader.Loader`.
It's fed an appropriate set of restrictions, and the path to the kustomization.
- A plugin loader.
It finds plugins (transformers, generators or validators)
and prepares them for running.
- A `KustTarget` encapsulating all of the above.
A KustTarget contains one `Kustomization` and represents
everything that kustomization can reach. This will include
other `KustTarget` instances, each having a smaller purview than
the one referencing it.
- Call `KustTarget.Load` to load its kustomization.
This step deals with deprecations and field changes.
- Load [openapi] data specified by the kustomization.
This is needed to recognize k8s kinds and their special
properties, e.g. which kinds are cluster-scoped, which kinds
refer to others, etc.
- Call `KustTarget.makeCustomizedResmap` to create the `ResMap` result.
This visits everything referenced by the kustomization,
performing all generation, transformation and validation.
- Finish the `Run` with
- Optional reordering of objects in `ResMap`, overriding the
FIFO rule.
- Optional addition of _kustomize build annotations_ to the
resources. E.g. from which repo and file the resource was
read, the fact that kustomize touched the resource, etc.
These kustomize-specific annotations are intended for
server-side data analytics, file structure traceability and
reconstruction, etc.
### The `makeCustomizedResmap` function
This function starts the process of object transformation,
as well as accumulation of recursively referenced data.
- Call `ra := KustTarget.AccumulateTarget`.
The result, `ra`, is a resource accumulator that contains
everything referred to by the current kustomization, now fully
hydrated.
- Uniquify names of generated objects by appending content hashes.
This cannot be done until the objects are complete.
- Fix all name references (given that names may have changed).
E.g. if a ConfigMaps was given a generated name, all objects that
refer to that ConfigMap must be given its name.
- Resolve vars, replacing them with whatever they refer to (a legacy feature).
### The `AccumulateTarget` function
- Call `AccumulateResources` over the `resources` field (this can recurse).
- Call `AccumulateComponents` over the `components` field (this can recurse),
- Load legacy (pre-plugin) global kustomize configuration,
- Load legacy (pre-openapi) _Custom Resource Definition_ data.
- In the context of the data loaded above, run the kustomization's
- generators,
- transformers,
- and validators.
- Accumulate `vars` (make note of them for later replacement).
### `AccumulateResources` and component accumulation
- If the path is a file:
- Accumulate the objects in the file (treating them
as opaque kubernetes objects).
- If the path is a directory:
- Create a new `KustTarget` referring to that directory's kustomization.
- Call `subRa := KustTarget.AccumulateTarget`.
- Call `ra.MergeAccumulator(subRa)`
This completes a recursion.
- If the path is a git URL:
- Clone the repository to a temporary directory.
- Process the path optionally specified in the URL
as a path in the clone.
- If no path specified, work from the repository root.
That's as deep as this discussion will go.
The deeper this document goes into the details, the faster
it will get out of date.

View File

@@ -1,25 +1,6 @@
[SIG-CLI]: https://github.com/kubernetes/community/tree/master/sig-cli
[Slack channel]: https://kubernetes.slack.com/messages/kustomize
[Mailing list]: https://groups.google.com/forum/#!forum/kubernetes-sig-cli
[OWNERS file spec]: https://github.com/kubernetes/community/blob/master/contributors/guide/owners.md
[Kustomize OWNERS_ALIASES]: https://github.com/kubernetes-sigs/kustomize/blob/8049f7b1af52e8a7ec26faf6cf714f560d0043c5/OWNERS_ALIASES
[SIG-CLI Teams]: https://github.com/kubernetes/org/blob/main/config/kubernetes-sigs/sig-cli/teams.yaml
[Github permissions]: https://docs.github.com/en/organizations/managing-access-to-your-organizations-repositories/repository-permission-levels-for-an-organization#repository-access-for-each-permission-level
[Contributor License Agreement]: https://git.k8s.io/community/CLA.md
[Kubernetes Contributor Guide]: http://git.k8s.io/community/contributors/guide
[Contributor Cheat Sheet]: https://git.k8s.io/community/contributors/guide/contributor-cheatsheet/README.md
[CNCF Code of Conduct]: https://github.com/cncf/foundation/blob/master/code-of-conduct.md
[Kubernetes Community Membership]: https://github.com/kubernetes/community/blob/master/community-membership.md
[Contribution Guide]: https://kubectl.docs.kubernetes.io/contributing/kustomize/
[MacOS Dev Guide]: https://kubectl.docs.kubernetes.io/contributing/kustomize/mac/
[Windows Dev Guide]: https://kubectl.docs.kubernetes.io/contributing/kustomize/windows/
# Contributing Guidelines # Contributing Guidelines
Welcome to Kubernetes. We are excited about the prospect of you joining our [community](https://github.com/kubernetes/community)! The Kubernetes community abides by the [CNCF Code of Conduct]. Here is an excerpt: Welcome to Kubernetes. We are excited about the prospect of you joining our [community](https://github.com/kubernetes/community)! The Kubernetes community abides by the CNCF [code of conduct](code-of-conduct.md). Here is an excerpt:
_As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities._ _As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities._
@@ -27,45 +8,19 @@ _As contributors and maintainers of this project, and in the interest of fosteri
Dev guides: Dev guides:
- [Contribution Guide] - [Mac](docs/macDevGuide.md)
- [MacOS Dev Guide]
- [Windows Dev Guide]
General resources for contributors: We have full documentation on how to get started contributing here:
- [Contributor License Agreement] - Kubernetes projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests. - [Contributor License Agreement](https://git.k8s.io/community/CLA.md) Kubernetes projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests
- [Kubernetes Contributor Guide] - Main contributor documentation. - [Kubernetes Contributor Guide](http://git.k8s.io/community/contributors/guide) - Main contributor documentation, or you can just jump directly to the [contributing section](http://git.k8s.io/community/contributors/guide#contributing)
- [Contributor Cheat Sheet] - Common resources for existing developers. - [Contributor Cheat Sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet/README.md) - Common resources for existing developers
Here are some additional ideas to help you get started with Kustomize:
- Attend a Kustomize Bug Scrub. Check the [SIG-CLI] meetings list to find the next one.
- Help triage issues by confirming validity and applying the appropriate `kind` label (e.g. comment `/kind bug`).
- Pick up an issue to fix. Issues with the `help-wanted` label are a good place to start, but you can also look for any issue with the `triage/accepted` label and no assignee. Remember to `/assign` yourself to let others know you're working on it.
- Help confirm new issues labelled `kind/bug` by reproducing them with the latest release.
- Support Kustomize users by responding to questions on issues labelled `kind/support` or in the [Slack channel].
## Mentorship ## Mentorship
- [Mentoring Initiatives](https://git.k8s.io/community/mentoring) - We have a diverse set of mentorship programs available that are always looking for volunteers! - [Mentoring Initiatives](https://git.k8s.io/community/mentoring) - We have a diverse set of mentorship programs available that are always looking for volunteers!
## Contributor Ladder
Kustomize follows the [Kubernetes Community Membership] contributor ladder. Roles are as follows:
1. Contributor: Anyone who actively contributes code, issues or reviews to the project. All contributors must sign the [Contributor License Agreement].
1. Reviewer: Contributors with a history of review and authorship on Kustomize. Has LGTM rights on the Kustomize repo (as do all kubernetes-sigs org members). Active contributors are encouraged to join the reviewers list to be automatically pinged on PRs.
1. Approver: Highly experienced active reviewer and contributor to Kustomize. Has both LTGM and approval rights on the Kustomize repo, as well as "maintain" [Github permissions].
1. Owner: Approver who sets technical direction and makes or approves design decisions for the project. Has LGTM and approval rights on the Kustomize repo as well as "admin" [Github permissions].
The kyaml module within the Kustomize repo has additional owners following the same ladder.
Administrative notes:
- The [OWNERS file spec] is a useful resources in making changes.
- Maintainers and admins must be added to the appropriate lists in both [Kustomize OWNERS_ALIASES] and [SIG-CLI Teams]. If this isn't done, the individual in question will lack either PR approval rights (Kustomize list) or the appropriate Github repository permissions (community list).
## Contact Information ## Contact Information
- [Slack channel] - [Slack channel](https://kubernetes.slack.com/messages/sig-cli)
- [Mailing list] - [Mailing list](https://groups.google.com/forum/#!forum/kubernetes-sig-cli)

183
Makefile
View File

@@ -3,87 +3,77 @@
# #
# Makefile for kustomize CLI and API. # Makefile for kustomize CLI and API.
SHELL := /usr/bin/env bash MYGOBIN := $(shell go env GOPATH)/bin
GOOS = $(shell go env GOOS) SHELL := /bin/bash
GOARCH = $(shell go env GOARCH)
MYGOBIN = $(shell go env GOBIN)
ifeq ($(MYGOBIN),)
MYGOBIN = $(shell go env GOPATH)/bin
endif
export PATH := $(MYGOBIN):$(PATH) export PATH := $(MYGOBIN):$(PATH)
MODULES := '"cmd/config" "api/" "kustomize/" "kyaml/"'
LATEST_V4_RELEASE=v4.5.1
# Provide defaults for REPO_OWNER and REPO_NAME if not present.
# Typically these values would be provided by Prow.
ifndef REPO_OWNER
REPO_OWNER := "kubernetes-sigs"
endif
ifndef REPO_NAME
REPO_NAME := "kustomize"
endif
.PHONY: all .PHONY: all
all: install-tools verify-kustomize all: verify-kustomize
.PHONY: verify-kustomize .PHONY: verify-kustomize
verify-kustomize: \ verify-kustomize: \
lint-kustomize \ lint-kustomize \
test-unit-kustomize-all \ test-unit-kustomize-all \
test-examples-kustomize-against-HEAD \ test-examples-kustomize-against-HEAD \
test-examples-kustomize-against-v4-release test-examples-kustomize-against-3.8.0
# The following target referenced by a file in # The following target referenced by a file in
# https://github.com/kubernetes/test-infra/tree/master/config/jobs/kubernetes-sigs/kustomize # https://github.com/kubernetes/test-infra/tree/master/config/jobs/kubernetes-sigs/kustomize
.PHONY: prow-presubmit-check .PHONY: prow-presubmit-check
prow-presubmit-check: \ prow-presubmit-check: \
install-tools \
lint-kustomize \ lint-kustomize \
test-unit-kustomize-all \ test-unit-kustomize-all \
test-unit-cmd-all \ test-unit-cmd-all \
test-go-mod \ test-go-mod \
test-examples-kustomize-against-HEAD \ test-examples-kustomize-against-HEAD \
test-examples-kustomize-against-v4-release test-examples-kustomize-against-3.8.0
.PHONY: verify-kustomize-e2e .PHONY: verify-kustomize-e2e
verify-kustomize-e2e: test-examples-e2e-kustomize verify-kustomize-e2e: test-examples-e2e-kustomize
# Other builds in this repo might want a different linter version. # Other builds in this repo might want a different linter version.
# Without one Makefile to rule them all, the different makes # Without one Makefile to rule them all, the different makes
# cannot assume that golanci-lint is at the version they want. # cannot assume that golanci-lint is at the version they want
# This installs what kustomize wants to use. # since everything uses the same implicit GOPATH.
# This installs in a temp dir to avoid overwriting someone else's
# linter, then installs in MYGOBIN with a new name.
# Version pinned by hack/go.mod
$(MYGOBIN)/golangci-lint-kustomize: $(MYGOBIN)/golangci-lint-kustomize:
rm -f $(CURDIR)/hack/golangci-lint ( \
GOBIN=$(CURDIR)/hack go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.23.8 set -e; \
mv $(CURDIR)/hack/golangci-lint $(MYGOBIN)/golangci-lint-kustomize cd hack; \
GO111MODULE=on go build -tags=tools -o $(MYGOBIN)/golangci-lint-kustomize github.com/golangci/golangci-lint/cmd/golangci-lint; \
)
# Version pinned by api/go.mod
$(MYGOBIN)/mdrip: $(MYGOBIN)/mdrip:
go install github.com/monopole/mdrip@v1.0.2 cd api; \
go install github.com/monopole/mdrip
# Version pinned by api/go.mod
$(MYGOBIN)/stringer: $(MYGOBIN)/stringer:
go get golang.org/x/tools/cmd/stringer cd api; \
go install golang.org/x/tools/cmd/stringer
# Version pinned by api/go.mod
$(MYGOBIN)/goimports: $(MYGOBIN)/goimports:
go get golang.org/x/tools/cmd/goimports cd api; \
go install golang.org/x/tools/cmd/goimports
# Build from local source. # Install resource from whatever is checked out.
$(MYGOBIN)/gorepomod: $(MYGOBIN)/resource:
cd cmd/gorepomod; \ cd cmd/resource; \
go install . go install .
# Build from local source. # To pin pluginator, use this recipe instead:
$(MYGOBIN)/k8scopy: # cd api;
cd cmd/k8scopy; \ # go install sigs.k8s.io/kustomize/pluginator/v2
go install .
# Build from local source.
$(MYGOBIN)/pluginator: $(MYGOBIN)/pluginator:
cd cmd/pluginator; \ cd pluginator; \
go install . go install .
# Build from local source. # Install kustomize from whatever is checked out.
$(MYGOBIN)/kustomize: build-kustomize-api $(MYGOBIN)/kustomize:
cd kustomize; \ cd kustomize; \
go install . go install .
@@ -91,9 +81,6 @@ $(MYGOBIN)/kustomize: build-kustomize-api
install-tools: \ install-tools: \
$(MYGOBIN)/goimports \ $(MYGOBIN)/goimports \
$(MYGOBIN)/golangci-lint-kustomize \ $(MYGOBIN)/golangci-lint-kustomize \
$(MYGOBIN)/gorepomod \
$(MYGOBIN)/helmV3 \
$(MYGOBIN)/k8scopy \
$(MYGOBIN)/mdrip \ $(MYGOBIN)/mdrip \
$(MYGOBIN)/pluginator \ $(MYGOBIN)/pluginator \
$(MYGOBIN)/stringer $(MYGOBIN)/stringer
@@ -120,14 +107,13 @@ install-tools: \
# module (it's linked into the api). # module (it's linked into the api).
# Where all generated builtin plugin code should go. # Where all generated builtin plugin code should go.
pGen=api/internal/builtins pGen=api/builtins
# Where the builtin Go plugin modules live. # Where the builtin Go plugin modules live.
pSrc=plugin/builtin pSrc=plugin/builtin
_builtinplugins = \ _builtinplugins = \
AnnotationsTransformer.go \ AnnotationsTransformer.go \
ConfigMapGenerator.go \ ConfigMapGenerator.go \
IAMPolicyGenerator.go \
HashTransformer.go \ HashTransformer.go \
ImageTagTransformer.go \ ImageTagTransformer.go \
LabelTransformer.go \ LabelTransformer.go \
@@ -136,13 +122,10 @@ _builtinplugins = \
PatchJson6902Transformer.go \ PatchJson6902Transformer.go \
PatchStrategicMergeTransformer.go \ PatchStrategicMergeTransformer.go \
PatchTransformer.go \ PatchTransformer.go \
PrefixTransformer.go \ PrefixSuffixTransformer.go \
SuffixTransformer.go \
ReplacementTransformer.go \
ReplicaCountTransformer.go \ ReplicaCountTransformer.go \
SecretGenerator.go \ SecretGenerator.go \
ValueAddTransformer.go \ ValueAddTransformer.go
HelmChartInflationGenerator.go
# Maintaining this explicit list of generated files, and # Maintaining this explicit list of generated files, and
# adding it as a dependency to a few targets, to assure # adding it as a dependency to a few targets, to assure
@@ -156,7 +139,6 @@ builtinplugins = $(patsubst %,$(pGen)/%,$(_builtinplugins))
# that file, will be recreated. # that file, will be recreated.
$(pGen)/AnnotationsTransformer.go: $(pSrc)/annotationstransformer/AnnotationsTransformer.go $(pGen)/AnnotationsTransformer.go: $(pSrc)/annotationstransformer/AnnotationsTransformer.go
$(pGen)/ConfigMapGenerator.go: $(pSrc)/configmapgenerator/ConfigMapGenerator.go $(pGen)/ConfigMapGenerator.go: $(pSrc)/configmapgenerator/ConfigMapGenerator.go
$(pGen)/GkeSaGenerator.go: $(pSrc)/gkesagenerator/GkeSaGenerator.go
$(pGen)/HashTransformer.go: $(pSrc)/hashtransformer/HashTransformer.go $(pGen)/HashTransformer.go: $(pSrc)/hashtransformer/HashTransformer.go
$(pGen)/ImageTagTransformer.go: $(pSrc)/imagetagtransformer/ImageTagTransformer.go $(pGen)/ImageTagTransformer.go: $(pSrc)/imagetagtransformer/ImageTagTransformer.go
$(pGen)/LabelTransformer.go: $(pSrc)/labeltransformer/LabelTransformer.go $(pGen)/LabelTransformer.go: $(pSrc)/labeltransformer/LabelTransformer.go
@@ -165,13 +147,10 @@ $(pGen)/NamespaceTransformer.go: $(pSrc)/namespacetransformer/NamespaceTransform
$(pGen)/PatchJson6902Transformer.go: $(pSrc)/patchjson6902transformer/PatchJson6902Transformer.go $(pGen)/PatchJson6902Transformer.go: $(pSrc)/patchjson6902transformer/PatchJson6902Transformer.go
$(pGen)/PatchStrategicMergeTransformer.go: $(pSrc)/patchstrategicmergetransformer/PatchStrategicMergeTransformer.go $(pGen)/PatchStrategicMergeTransformer.go: $(pSrc)/patchstrategicmergetransformer/PatchStrategicMergeTransformer.go
$(pGen)/PatchTransformer.go: $(pSrc)/patchtransformer/PatchTransformer.go $(pGen)/PatchTransformer.go: $(pSrc)/patchtransformer/PatchTransformer.go
$(pGen)/PrefixTransformer.go: $(pSrc)/prefixtransformer/PrefixTransformer.go $(pGen)/PrefixSuffixTransformer.go: $(pSrc)/prefixsuffixtransformer/PrefixSuffixTransformer.go
$(pGen)/SuffixTransformer.go: $(pSrc)/suffixtransformer/SuffixTransformer.go
$(pGen)/ReplacementTransformer.go: $(pSrc)/replacementtransformer/ReplacementTransformer.go
$(pGen)/ReplicaCountTransformer.go: $(pSrc)/replicacounttransformer/ReplicaCountTransformer.go $(pGen)/ReplicaCountTransformer.go: $(pSrc)/replicacounttransformer/ReplicaCountTransformer.go
$(pGen)/SecretGenerator.go: $(pSrc)/secretgenerator/SecretGenerator.go $(pGen)/SecretGenerator.go: $(pSrc)/secretgenerator/SecretGenerator.go
$(pGen)/ValueAddTransformer.go: $(pSrc)/valueaddtransformer/ValueAddTransformer.go $(pGen)/ValueAddTransformer.go: $(pSrc)/valueaddtransformer/ValueAddTransformer.go
$(pGen)/HelmChartInflationGenerator.go: $(pSrc)/helmchartinflationgenerator/HelmChartInflationGenerator.go
# The (verbose but portable) Makefile way to convert to lowercase. # The (verbose but portable) Makefile way to convert to lowercase.
toLowerCase = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$1)))))))))))))))))))))))))) toLowerCase = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$1))))))))))))))))))))))))))
@@ -190,27 +169,24 @@ $(pGen)/%.go: $(MYGOBIN)/pluginator
.PHONY: generate-kustomize-builtin-plugins .PHONY: generate-kustomize-builtin-plugins
generate-kustomize-builtin-plugins: $(builtinplugins) generate-kustomize-builtin-plugins: $(builtinplugins)
.PHONY: build-kustomize-external-go-plugin .PHONY: kustomize-external-go-plugin-build
build-kustomize-external-go-plugin: kustomize-external-go-plugin-build:
./hack/buildExternalGoPlugins.sh ./plugin ./hack/buildExternalGoPlugins.sh ./plugin
.PHONY: clean-kustomize-external-go-plugin .PHONY: kustomize-external-go-plugin-clean
clean-kustomize-external-go-plugin: kustomize-external-go-plugin-clean:
./hack/buildExternalGoPlugins.sh ./plugin clean ./hack/buildExternalGoPlugins.sh ./plugin clean
### End kustomize plugin rules. ### End kustomize plugin rules.
.PHONY: lint-kustomize .PHONY: lint-kustomize
lint-kustomize: $(MYGOBIN)/golangci-lint-kustomize $(builtinplugins) lint-kustomize: install-tools $(builtinplugins)
cd api; $(MYGOBIN)/golangci-lint-kustomize \ cd api; \
-c ../.golangci-kustomize.yml \ $(MYGOBIN)/golangci-lint-kustomize -c ../.golangci-kustomize.yml run ./...
run ./... cd kustomize; \
cd kustomize; $(MYGOBIN)/golangci-lint-kustomize \ $(MYGOBIN)/golangci-lint-kustomize -c ../.golangci-kustomize.yml run ./...
-c ../.golangci-kustomize.yml \ cd pluginator; \
run ./... $(MYGOBIN)/golangci-lint-kustomize -c ../.golangci-kustomize.yml run ./...
cd cmd/pluginator; $(MYGOBIN)/golangci-lint-kustomize \
-c ../../.golangci-kustomize.yml \
run ./...
# Used to add non-default compilation flags when experimenting with # Used to add non-default compilation flags when experimenting with
# plugin-to-api compatibility checks. # plugin-to-api compatibility checks.
@@ -218,14 +194,9 @@ lint-kustomize: $(MYGOBIN)/golangci-lint-kustomize $(builtinplugins)
build-kustomize-api: $(builtinplugins) build-kustomize-api: $(builtinplugins)
cd api; go build ./... cd api; go build ./...
.PHONY: generate-kustomize-api
generate-kustomize-api: $(MYGOBIN)/k8scopy
cd api; go generate ./...
.PHONY: test-unit-kustomize-api .PHONY: test-unit-kustomize-api
test-unit-kustomize-api: build-kustomize-api test-unit-kustomize-api: build-kustomize-api
cd api; go test ./... -ldflags "-X sigs.k8s.io/kustomize/api/provenance.version=v444.333.222" cd api; go test ./... -ldflags "-X sigs.k8s.io/kustomize/api/provenance.version=v444.333.222"
cd api/krusty; OPENAPI_TEST=true go test -run TestCustomOpenAPIFieldFromComponentWithOverlays
.PHONY: test-unit-kustomize-plugins .PHONY: test-unit-kustomize-plugins
test-unit-kustomize-plugins: test-unit-kustomize-plugins:
@@ -242,10 +213,10 @@ test-unit-kustomize-all: \
test-unit-kustomize-plugins test-unit-kustomize-plugins
test-unit-cmd-all: test-unit-cmd-all:
./hack/kyaml-pre-commit.sh ./travis/kyaml-pre-commit.sh
test-go-mod: test-go-mod:
./hack/check-go-mod.sh ./travis/check-go-mod.sh
.PHONY: .PHONY:
test-examples-e2e-kustomize: $(MYGOBIN)/mdrip $(MYGOBIN)/kind test-examples-e2e-kustomize: $(MYGOBIN)/mdrip $(MYGOBIN)/kind
@@ -262,8 +233,17 @@ test-examples-kustomize-against-HEAD: $(MYGOBIN)/kustomize $(MYGOBIN)/mdrip
./hack/testExamplesAgainstKustomize.sh HEAD ./hack/testExamplesAgainstKustomize.sh HEAD
.PHONY: .PHONY:
test-examples-kustomize-against-v4-release: $(MYGOBIN)/mdrip test-examples-kustomize-against-3.8.0: $(MYGOBIN)/mdrip
./hack/testExamplesAgainstKustomize.sh v4@$(LATEST_V4_RELEASE) ( \
set -e; \
tag=v3.8.0; \
/bin/rm -f $(MYGOBIN)/kustomize; \
echo "Installing kustomize $$tag."; \
GO111MODULE=on go get sigs.k8s.io/kustomize/kustomize/v3@$${tag}; \
./hack/testExamplesAgainstKustomize.sh $$tag; \
echo "Reinstalling kustomize from HEAD."; \
cd kustomize; go install .; \
)
# linux only. # linux only.
# This is for testing an example plugin that # This is for testing an example plugin that
@@ -275,8 +255,8 @@ $(MYGOBIN)/kubeval:
( \ ( \
set -e; \ set -e; \
d=$(shell mktemp -d); cd $$d; \ d=$(shell mktemp -d); cd $$d; \
wget https://github.com/instrumenta/kubeval/releases/latest/download/kubeval-$(GOOS)-$(GOARCH).tar.gz; \ wget https://github.com/instrumenta/kubeval/releases/latest/download/kubeval-linux-amd64.tar.gz; \
tar xf kubeval-$(GOOS)-$(GOARCH).tar.gz; \ tar xf kubeval-linux-amd64.tar.gz; \
mv kubeval $(MYGOBIN); \ mv kubeval $(MYGOBIN); \
rm -rf $$d; \ rm -rf $$d; \
) )
@@ -290,10 +270,10 @@ $(MYGOBIN)/helmV2:
( \ ( \
set -e; \ set -e; \
d=$(shell mktemp -d); cd $$d; \ d=$(shell mktemp -d); cd $$d; \
tgzFile=helm-v2.13.1-$(GOOS)-$(GOARCH).tar.gz; \ tgzFile=helm-v2.13.1-linux-amd64.tar.gz; \
wget https://storage.googleapis.com/kubernetes-helm/$$tgzFile; \ wget https://storage.googleapis.com/kubernetes-helm/$$tgzFile; \
tar -xvzf $$tgzFile; \ tar -xvzf $$tgzFile; \
mv $(GOOS)-$(GOARCH)/helm $(MYGOBIN)/helmV2; \ mv linux-amd64/helm $(MYGOBIN)/helmV2; \
rm -rf $$d \ rm -rf $$d \
) )
@@ -303,47 +283,34 @@ $(MYGOBIN)/helmV3:
( \ ( \
set -e; \ set -e; \
d=$(shell mktemp -d); cd $$d; \ d=$(shell mktemp -d); cd $$d; \
tgzFile=helm-v3.6.3-$(GOOS)-$(GOARCH).tar.gz; \ tgzFile=helm-v3.2.0-rc.1-linux-amd64.tar.gz; \
wget https://get.helm.sh/$$tgzFile; \ wget https://get.helm.sh/$$tgzFile; \
tar -xvzf $$tgzFile; \ tar -xvzf $$tgzFile; \
mv $(GOOS)-$(GOARCH)/helm $(MYGOBIN)/helmV3; \ mv linux-amd64/helm $(MYGOBIN)/helmV3; \
rm -rf $$d \ rm -rf $$d \
) )
# Default version of helm is v2 for the time being.
$(MYGOBIN)/helm: $(MYGOBIN)/helmV2
ln -s $(MYGOBIN)/helmV2 $(MYGOBIN)/helm
$(MYGOBIN)/kind: $(MYGOBIN)/kind:
( \ ( \
set -e; \ set -e; \
d=$(shell mktemp -d); cd $$d; \ d=$(shell mktemp -d); cd $$d; \
wget -O ./kind https://github.com/kubernetes-sigs/kind/releases/download/v0.7.0/kind-$(GOOS)-$(GOARCH); \ wget -O ./kind https://github.com/kubernetes-sigs/kind/releases/download/v0.7.0/kind-$(shell uname)-amd64; \
chmod +x ./kind; \ chmod +x ./kind; \
mv ./kind $(MYGOBIN); \ mv ./kind $(MYGOBIN); \
rm -rf $$d; \ rm -rf $$d; \
) )
# linux only.
$(MYGOBIN)/gh:
( \
set -e; \
d=$(shell mktemp -d); cd $$d; \
tgzFile=gh_1.0.0_$(GOOS)_$(GOARCH).tar.gz; \
wget https://github.com/cli/cli/releases/download/v1.0.0/$$tgzFile; \
tar -xvzf $$tgzFile; \
mv gh_1.0.0_$(GOOS)_$(GOARCH)/bin/gh $(MYGOBIN)/gh; \
rm -rf $$d \
)
.PHONY: clean .PHONY: clean
clean: clean-kustomize-external-go-plugin clean: kustomize-external-go-plugin-clean
go clean --cache go clean --cache
rm -f $(builtinplugins) rm -f $(builtinplugins)
rm -f $(MYGOBIN)/goimports rm -f $(MYGOBIN)/pluginator
rm -f $(MYGOBIN)/golangci-lint-kustomize
rm -f $(MYGOBIN)/kustomize rm -f $(MYGOBIN)/kustomize
rm -f $(MYGOBIN)/mdrip rm -f $(MYGOBIN)/golangci-lint-kustomize
rm -f $(MYGOBIN)/stringer
# Handle pluginator manually.
# rm -f $(MYGOBIN)/pluginator
# Nuke the site from orbit. It's the only way to be sure. # Nuke the site from orbit. It's the only way to be sure.
.PHONY: nuke .PHONY: nuke

6
OWNERS
View File

@@ -1,6 +1,4 @@
# See https://github.com/kubernetes/community/blob/master/community-membership.md # See https://github.com/kubernetes/community/blob/master/community-membership.md
approvers: approvers:
- kustomize-approvers - kustomize-admins
- kustomize-maintainers
reviewers:
- kustomize-reviewers

View File

@@ -1,29 +1,13 @@
# Keep *-owners and *-approvers lists in sync with *-admins and *-maintainers in
# https://github.com/kubernetes/org/blob/main/config/kubernetes-sigs/sig-cli/teams.yaml
aliases: aliases:
kustomize-owners: kustomize-admins:
- knverey
- natasha41575
kustomize-approvers:
- knverey
- natasha41575
kustomize-reviewers:
- knverey
- natasha41575
- yuwenma
kyaml-approvers:
- mengqiy
- mortent
- phanimarupaka
kyaml-reviewers:
- mengqiy
- mortent
- phanimarupaka
emeritus-approvers:
- liujingfang1
- Shell32-Natsu
- justinsb
- monopole - monopole
- pwittrock - pwittrock
kustomize-maintainers:
- droot
- justinsb
- liujingfang1
- mengqiy
- monopole
- pwittrock
- mortent
- phanimarupaka

View File

@@ -20,27 +20,15 @@ This tool is sponsored by [sig-cli] ([KEP]).
## kubectl integration ## kubectl integration
The kustomize build flow at [v2.0.3] was added Since [v1.14][kubectl announcement] the kustomize build system has been included in kubectl.
to [kubectl v1.14][kubectl announcement]. The kustomize
flow in kubectl remained frozen at v2.0.3 until kubectl v1.21,
which [updated it to v4.0.5][kust-in-kubectl update]. It will
be updated on a regular basis going forward, and such updates
will be reflected in the Kubernetes release notes.
| Kubectl version | Kustomize version | | kubectl version | kustomize version |
| --- | --- | |---------|--------|
| < v1.14 | n/a | | v1.16.0 | [v2.0.3](/../../tree/v2.0.3) |
| v1.14-v1.20 | v2.0.3 | | v1.15.x | [v2.0.3](/../../tree/v2.0.3) |
| v1.21 | v4.0.5 | | v1.14.x | [v2.0.3](/../../tree/v2.0.3) |
| v1.22 | v4.2.0 |
[v2.0.3]: /../../tree/v2.0.3 For examples and guides for using the kubectl integration please see the [kubectl book] or the [kubernetes documentation].
[#2506]: https://github.com/kubernetes-sigs/kustomize/issues/2506
[#1500]: https://github.com/kubernetes-sigs/kustomize/issues/1500
[kust-in-kubectl update]: https://github.com/kubernetes/kubernetes/blob/4d75a6238a6e330337526e0513e67d02b1940b63/CHANGELOG/CHANGELOG-1.21.md#kustomize-updates-in-kubectl
For examples and guides for using the kubectl integration please
see the [kubectl book] or the [kubernetes documentation].
## Usage ## Usage
@@ -152,7 +140,7 @@ is governed by the [Kubernetes Code of Conduct].
[`make`]: https://www.gnu.org/software/make [`make`]: https://www.gnu.org/software/make
[`sed`]: https://www.gnu.org/software/sed [`sed`]: https://www.gnu.org/software/sed
[DAM]: https://kubernetes-sigs.github.io/kustomize/api-reference/glossary#declarative-application-management [DAM]: https://kubernetes-sigs.github.io/kustomize/api-reference/glossary#declarative-application-management
[KEP]: https://github.com/kubernetes/enhancements/blob/master/keps/sig-cli/2377-Kustomize/README.md [KEP]: https://github.com/kubernetes/enhancements/blob/master/keps/sig-cli/0008-kustomize.md
[Kubernetes Code of Conduct]: code-of-conduct.md [Kubernetes Code of Conduct]: code-of-conduct.md
[applied]: https://kubernetes-sigs.github.io/kustomize/api-reference/glossary#apply [applied]: https://kubernetes-sigs.github.io/kustomize/api-reference/glossary#apply
[base]: https://kubernetes-sigs.github.io/kustomize/api-reference/glossary#base [base]: https://kubernetes-sigs.github.io/kustomize/api-reference/glossary#base
@@ -160,7 +148,7 @@ is governed by the [Kubernetes Code of Conduct].
[imageBase]: docs/images/base.jpg [imageBase]: docs/images/base.jpg
[imageOverlay]: docs/images/overlay.jpg [imageOverlay]: docs/images/overlay.jpg
[kubectl announcement]: 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/guides/introduction/kustomize/ [kubectl book]: https://kubectl.docs.kubernetes.io/pages/app_customization/introduction.html
[kubernetes documentation]: https://kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/ [kubernetes documentation]: https://kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/
[kubernetes style]: https://kubernetes-sigs.github.io/kustomize/api-reference/glossary#kubernetes-style-object [kubernetes style]: https://kubernetes-sigs.github.io/kustomize/api-reference/glossary#kubernetes-style-object
[kustomization]: https://kubernetes-sigs.github.io/kustomize/api-reference/glossary#kustomization [kustomization]: https://kubernetes-sigs.github.io/kustomize/api-reference/glossary#kustomization

View File

@@ -1,112 +0,0 @@
# Kustomize roadmap 2022
Presented at the [January 26, 2022, SIG-CLI meeting](https://youtu.be/l2plzJ9MRlk?t=1321)
kustomize maintainers: @knverey, @natasha41575
[Objective: Improve contributor community](https://github.com/kubernetes-sigs/kustomize/blob/master/roadmap-2021.md#objective-improve-contributor-community)
[Objective: Improve end-user experience](https://github.com/kubernetes-sigs/kustomize/blob/master/roadmap-2021.md#objective-improve-end-user-experience)
[Objective: Improve extension experience](https://github.com/kubernetes-sigs/kustomize/blob/master/roadmap-2021.md#objective-improve-extension-experience)
## Objective: Improve contributor community
**_WHO: End user who also contributes source code._**
Top priority:
- Kustomization v1 (also end-user impact) ([PROJECT](https://github.com/kubernetes-sigs/kustomize/projects/12))
- Remove the following fields:
- [vars](https://github.com/kubernetes-sigs/kustomize/issues/2052)
- [patchesJson6902, patchesStrategicMerge (consolidate on \`patches)](https://github.com/kubernetes-sigs/kustomize/issues/4376)
- [helmChartInflationGenerator, helmCharts, helmGlobals](https://github.com/kubernetes-sigs/kustomize/issues/4401)
- all long-deprecated fields in Kustomization v1 such as \`bases\` and those being accommodate by kustomize edit \[[see code snippet](https://github.com/kubernetes-sigs/kustomize/blob/ee4b7847f0beb6c0d2070673b10f23f7b3e92e82/api/types/fix.go#L15)\]
- Ensure that \`kustomize edit fix\` handles migrations for all those, and that anything it changes is not still present in v1.
- [Add reorder field](https://github.com/kubernetes-sigs/kustomize/issues/3913). Default should be FIFO and legacy should also be supported (could add alphabetic and custom sort support eventually). Replaces -reorder flag.
- [Reconcile openapi and crds field](https://github.com/kubernetes-sigs/kustomize/issues/3944)
- [Consider deprecating configurations field](https://github.com/kubernetes-sigs/kustomize/issues/3945) (old, pre-plugin, pre-openapi global configuration)
- [Add a field to enable the managedby label](https://github.com/kubernetes-sigs/kustomize/issues/4047)
Second priority:
- Improve contributor documentation
- [Instructions to upgrade kustomize-in-kubectl](https://github.com/kubernetes-sigs/kustomize/issues/3951)
Also very valuable to the project:
- [Improve the release process](https://github.com/kubernetes-sigs/kustomize/issues/3952) to support regular biweekly releases [PROJECT](https://github.com/kubernetes-sigs/kustomize/projects/7)
- Release sigs.k8s.io/kustomize/api v1.0.0 [PROJECT](https://github.com/kubernetes-sigs/kustomize/projects/5)
- [Reduce the public surface of the API module](https://github.com/kubernetes-sigs/kustomize/issues/3942)
- [Vendor all transitive deps](https://github.com/kubernetes-sigs/kustomize/issues/3706). Since kustomize is in kubectl, we must do as kubectl does to manage deps, exposing new transitive deps in code review.
- Project administration
- [Rename master branch to main](https://github.com/kubernetes-sigs/kustomize/issues/3954)
## Objective: Improve end-user experience
**_WHO: End user that wants kustomize build artifacts (binaries, containers)._**
Top priorities:
- Bug fixes:
- Fix bugs in basic anchor support: [issue query](https://github.com/kubernetes-sigs/kustomize/issues?q=is%3Aopen+is%3Aissue+label%3Aarea%2Fanchors)
- integer keys support: [#3446](https://github.com/kubernetes-sigs/kustomize/issues/3446)
- kyaml not respecting \`$patch replace|retainKeys\`: [#2037](https://github.com/kubernetes-sigs/kustomize/issues/2037)
- kustomize removing quotes from namespace field values: [#4146](https://github.com/kubernetes-sigs/kustomize/issues/4146)
- Kustomize doesnt support metadata.generateName: [#641](https://github.com/kubernetes-sigs/kustomize/issues/641)
- Send kustomize CLI version number into kubectl ([kubectl issue](https://github.com/kubernetes/kubectl/issues/797) / [kustomize issue](https://github.com/kubernetes-sigs/kustomize/issues/1424))
- Kustomize performance investigations/improvements [PROJECT](https://github.com/kubernetes-sigs/kustomize/projects/13)
- [Support generic resource references in name reference tracking](https://github.com/kubernetes-sigs/kustomize/issues/3418)
- [KEP 4267: retain the resource origin and transformer data in annotations](https://github.com/kubernetes-sigs/kustomize/pull/4267)
Secondary priorities:
- kustomize cli v5 ([PROJECT](https://github.com/kubernetes-sigs/kustomize/projects/14))
- [Drop the --reorder flag](https://github.com/kubernetes-sigs/kustomize/issues/3947)
- [Graduate cfg read-only commands out of alpha](https://github.com/kubernetes-sigs/kustomize/issues/4090).
- [Drop the enable-managedby-label](https://github.com/kubernetes-sigs/kustomize/issues/4047)
- Drop old plugin-related fields in favor of [the Catalog-style fields](https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/2906-kustomize-function-catalog).
- [Drop the helm flags](https://github.com/kubernetes-sigs/kustomize/issues/4401)
- [Confusion around namespace replacement](https://github.com/kubernetes-sigs/kustomize/issues/880).
Also very valuable to the project:
- [Overinclusion of root directory error in error messages](https://github.com/kubernetes-sigs/kustomize/issues/4348)
- [Add kustomize localize command](https://github.com/kubernetes-sigs/kustomize/issues/3980)
- [Fix Windows support in test suite](https://github.com/kubernetes-sigs/kustomize/issues/4001)
- Improve end-user documentation [PROJECT](https://github.com/kubernetes-sigs/kustomize/projects/9)
## Objective: Improve extension experience
**_WHO: Plugin developers: end users who extend kustomize, but dont think about internals._**
This objective is described in detail in the [Kustomize Plugin Graduation KEP](https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/2953-kustomize-plugin-graduation) / [PROJECT](https://github.com/kubernetes-sigs/kustomize/projects/15) .
Top priorities:
- Fix core usability issues with KRM Function extensions:
- [Better errors for function config failures](https://github.com/kubernetes-sigs/kustomize/issues/4398)
- [Container KRM Mounts are not mounting via function parameters](https://github.com/kubernetes-sigs/kustomize/issues/4290)
- [Resolution of local file references in extensions transformer configuration](https://github.com/kubernetes-sigs/kustomize/issues/4154)
- [Do not silently ignore plugins when config has typo](https://github.com/kubernetes-sigs/kustomize/issues/4399)
- [KRM Exec Function can't locate executable when referencing a base](https://github.com/kubernetes-sigs/kustomize/issues/4347)
- Once core usability issues are fixed, [deprecate legacy exec and Go plugin support](https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/2953-kustomize-plugin-graduation)
- [Catalog KEP](https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/2906-kustomize-function-catalog)
Secondary priorities:
- [Remove Starlark support](https://github.com/kubernetes-sigs/kustomize/issues/4349)
- [Composition KEP](https://github.com/kubernetes/enhancements/pull/2300). The implementation is complete in [#4223](https://github.com/kubernetes-sigs/kustomize/pull/4323), but depends on:
- [Convert resources and components to be backed by a reusable generator](https://github.com/kubernetes-sigs/kustomize/issues/4402)
- [Enable explicitly invoked transformers to use default fieldSpecs](https://github.com/kubernetes-sigs/kustomize/issues/4404)
- [Enable built-in generators to be used in the transformers field ](https://github.com/kubernetes-sigs/kustomize/issues/4403)
Also very valuable to the project:
- [Improve docs for kyaml libraries](https://github.com/kubernetes-sigs/kustomize/issues/3950), especially by adding examples.
- [Create a reserved field for plugin runtime information](https://github.com/kubernetes-sigs/kustomize/issues/4405)
- [Develop new standard process for implementing builtin transformers](https://github.com/kubernetes-sigs/kustomize/issues/4400)

View File

@@ -7,6 +7,7 @@ import (
"sigs.k8s.io/kustomize/api/filters/annotations" "sigs.k8s.io/kustomize/api/filters/annotations"
"sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/filtersutil"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
@@ -24,13 +25,16 @@ func (p *AnnotationsTransformerPlugin) Config(
} }
func (p *AnnotationsTransformerPlugin) Transform(m resmap.ResMap) error { func (p *AnnotationsTransformerPlugin) Transform(m resmap.ResMap) error {
if len(p.Annotations) == 0 { for _, r := range m.Resources() {
return nil err := filtersutil.ApplyToJSON(annotations.Filter{
Annotations: p.Annotations,
FsSlice: p.FieldSpecs,
}, r)
if err != nil {
return err
}
} }
return m.ApplyFilter(annotations.Filter{ return nil
Annotations: p.Annotations,
FsSlice: p.FieldSpecs,
})
} }
func NewAnnotationsTransformerPlugin() resmap.TransformerPlugin { func NewAnnotationsTransformerPlugin() resmap.TransformerPlugin {

View File

@@ -11,7 +11,7 @@ import (
) )
type HashTransformerPlugin struct { type HashTransformerPlugin struct {
hasher ifc.KustHasher hasher ifc.KunstructuredHasher
} }
func (p *HashTransformerPlugin) Config( func (p *HashTransformerPlugin) Config(
@@ -24,11 +24,10 @@ func (p *HashTransformerPlugin) Config(
func (p *HashTransformerPlugin) Transform(m resmap.ResMap) error { func (p *HashTransformerPlugin) Transform(m resmap.ResMap) error {
for _, res := range m.Resources() { for _, res := range m.Resources() {
if res.NeedHashSuffix() { if res.NeedHashSuffix() {
h, err := res.Hash(p.hasher) h, err := p.hasher.Hash(res)
if err != nil { if err != nil {
return err return err
} }
res.StorePreviousId()
res.SetName(fmt.Sprintf("%s-%s", res.GetName(), h)) res.SetName(fmt.Sprintf("%s-%s", res.GetName(), h))
} }
} }

View File

@@ -0,0 +1,187 @@
// Code generated by pluginator on ImageTagTransformer; DO NOT EDIT.
// pluginator {unknown 1970-01-01T00:00:00Z }
package builtins
import (
"fmt"
"regexp"
"strings"
"sigs.k8s.io/kustomize/api/filters/imagetag"
"sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/filtersutil"
"sigs.k8s.io/yaml"
)
// Find matching image declarations and replace
// the name, tag and/or digest.
type ImageTagTransformerPlugin struct {
ImageTag types.Image `json:"imageTag,omitempty" yaml:"imageTag,omitempty"`
FieldSpecs []types.FieldSpec `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"`
}
func (p *ImageTagTransformerPlugin) Config(
_ *resmap.PluginHelpers, c []byte) (err error) {
p.ImageTag = types.Image{}
p.FieldSpecs = nil
return yaml.Unmarshal(c, p)
}
func (p *ImageTagTransformerPlugin) Transform(m resmap.ResMap) error {
for _, r := range m.Resources() {
// If you're here because someone expected any field containing
// the string "containers" or "initContainers" to get an image
// update (not just spec/template/spec/containers[], etc.) then
// a code change is needed. See api/filters/imagetag/legacy
// for the start of an implementation that won't use an
// allowlist like FsSlice, and instead walks the object looking
// for fields named containers or initContainers.
err := filtersutil.ApplyToJSON(imagetag.Filter{
ImageTag: p.ImageTag,
FsSlice: p.FieldSpecs,
}, r)
if err != nil {
return err
}
}
return nil
}
func (p *ImageTagTransformerPlugin) mutateImage(in interface{}) (interface{}, error) {
original, ok := in.(string)
if !ok {
return nil, fmt.Errorf("image path is not of type string but %T", in)
}
if !isImageMatched(original, p.ImageTag.Name) {
return original, nil
}
name, tag := split(original)
if p.ImageTag.NewName != "" {
name = p.ImageTag.NewName
}
if p.ImageTag.NewTag != "" {
tag = ":" + p.ImageTag.NewTag
}
if p.ImageTag.Digest != "" {
tag = "@" + p.ImageTag.Digest
}
return name + tag, nil
}
// findAndReplaceImage replaces the image name and
// tags inside one object.
// It searches the object for container session
// then loops though all images inside containers
// session, finds matched ones and update the
// image name and tag name
func (p *ImageTagTransformerPlugin) findAndReplaceImage(obj map[string]interface{}) error {
paths := []string{"containers", "initContainers"}
updated := false
for _, path := range paths {
containers, found := obj[path]
if found && containers != nil {
if _, err := p.updateContainers(containers); err != nil {
return err
}
updated = true
}
}
if !updated {
return p.findContainers(obj)
}
return nil
}
func (p *ImageTagTransformerPlugin) updateContainers(in interface{}) (interface{}, error) {
containers, ok := in.([]interface{})
if !ok {
return nil, fmt.Errorf(
"containers path is not of type []interface{} but %T", in)
}
for i := range containers {
container := containers[i].(map[string]interface{})
containerImage, found := container["image"]
if !found {
continue
}
imageName := containerImage.(string)
if isImageMatched(imageName, p.ImageTag.Name) {
newImage, err := p.mutateImage(imageName)
if err != nil {
return nil, err
}
container["image"] = newImage
}
}
return containers, nil
}
func (p *ImageTagTransformerPlugin) findContainers(obj map[string]interface{}) error {
for key := range obj {
switch typedV := obj[key].(type) {
case map[string]interface{}:
err := p.findAndReplaceImage(typedV)
if err != nil {
return err
}
case []interface{}:
for i := range typedV {
item := typedV[i]
typedItem, ok := item.(map[string]interface{})
if ok {
err := p.findAndReplaceImage(typedItem)
if err != nil {
return err
}
}
}
}
}
return nil
}
func isImageMatched(s, t string) bool {
// Tag values are limited to [a-zA-Z0-9_.{}-].
// Some tools like Bazel rules_k8s allow tag patterns with {} characters.
// More info: https://github.com/bazelbuild/rules_k8s/pull/423
pattern, _ := regexp.Compile("^" + t + "(@sha256)?(:[a-zA-Z0-9_.{}-]*)?$")
return pattern.MatchString(s)
}
// split separates and returns the name and tag parts
// from the image string using either colon `:` or at `@` separators.
// Note that the returned tag keeps its separator.
func split(imageName string) (name string, tag string) {
// check if image name contains a domain
// if domain is present, ignore domain and check for `:`
ic := -1
if slashIndex := strings.Index(imageName, "/"); slashIndex < 0 {
ic = strings.LastIndex(imageName, ":")
} else {
lastIc := strings.LastIndex(imageName[slashIndex:], ":")
// set ic only if `:` is present
if lastIc > 0 {
ic = slashIndex + lastIc
}
}
ia := strings.LastIndex(imageName, "@")
if ic < 0 && ia < 0 {
return imageName, ""
}
i := ic
if ia > 0 {
i = ia
}
name = imageName[:i]
tag = imageName[i:]
return
}
func NewImageTagTransformerPlugin() resmap.TransformerPlugin {
return &ImageTagTransformerPlugin{}
}

View File

@@ -7,6 +7,7 @@ import (
"sigs.k8s.io/kustomize/api/filters/labels" "sigs.k8s.io/kustomize/api/filters/labels"
"sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/filtersutil"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
@@ -24,13 +25,16 @@ func (p *LabelTransformerPlugin) Config(
} }
func (p *LabelTransformerPlugin) Transform(m resmap.ResMap) error { func (p *LabelTransformerPlugin) Transform(m resmap.ResMap) error {
if len(p.Labels) == 0 { for _, r := range m.Resources() {
return nil err := filtersutil.ApplyToJSON(labels.Filter{
Labels: p.Labels,
FsSlice: p.FieldSpecs,
}, r)
if err != nil {
return err
}
} }
return m.ApplyFilter(labels.Filter{ return nil
Labels: p.Labels,
FsSlice: p.FieldSpecs,
})
} }
func NewLabelTransformerPlugin() resmap.TransformerPlugin { func NewLabelTransformerPlugin() resmap.TransformerPlugin {

View File

@@ -0,0 +1,126 @@
// Code generated by pluginator on NamespaceTransformer; DO NOT EDIT.
// pluginator {unknown 1970-01-01T00:00:00Z }
package builtins
import (
"fmt"
"sigs.k8s.io/kustomize/api/filters/namespace"
"sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/resource"
"sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/filtersutil"
"sigs.k8s.io/yaml"
)
// Change or set the namespace of non-cluster level resources.
type NamespaceTransformerPlugin struct {
types.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
FieldSpecs []types.FieldSpec `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"`
}
func (p *NamespaceTransformerPlugin) Config(
_ *resmap.PluginHelpers, c []byte) (err error) {
p.Namespace = ""
p.FieldSpecs = nil
return yaml.Unmarshal(c, p)
}
func (p *NamespaceTransformerPlugin) Transform(m resmap.ResMap) error {
if len(p.Namespace) == 0 {
return nil
}
for _, r := range m.Resources() {
if len(r.Map()) == 0 {
// Don't mutate empty objects?
continue
}
err := filtersutil.ApplyToJSON(namespace.Filter{
Namespace: p.Namespace,
FsSlice: p.FieldSpecs,
}, r)
if err != nil {
return err
}
matches := m.GetMatchingResourcesByCurrentId(r.CurId().Equals)
if len(matches) != 1 {
return fmt.Errorf(
"namespace transformation produces ID conflict: %+v", matches)
}
}
return nil
}
// Special casing metadata.namespace since
// 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 (p *NamespaceTransformerPlugin) applicableFieldSpecs(id resid.ResId) []types.FieldSpec {
var res []types.FieldSpec
for _, fs := range p.FieldSpecs {
if id.IsSelected(&fs.Gvk) &&
(fs.Path != types.MetadataNamespacePath ||
(fs.Path == types.MetadataNamespacePath && id.IsNamespaceableKind())) {
res = append(res, fs)
}
}
return res
}
func (p *NamespaceTransformerPlugin) changeNamespace(
_ *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 p.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
}
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"] = p.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 p.Namespace, nil
} else {
return in, nil
}
default:
return in, nil
}
}
}
func NewNamespaceTransformerPlugin() resmap.TransformerPlugin {
return &NamespaceTransformerPlugin{}
}

View File

@@ -10,18 +10,19 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"sigs.k8s.io/kustomize/api/filters/patchjson6902" "sigs.k8s.io/kustomize/api/filters/patchjson6902"
"sigs.k8s.io/kustomize/api/ifc" "sigs.k8s.io/kustomize/api/ifc"
"sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/kio/kioutil" "sigs.k8s.io/kustomize/kyaml/filtersutil"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
type PatchJson6902TransformerPlugin struct { type PatchJson6902TransformerPlugin struct {
ldr ifc.Loader ldr ifc.Loader
decodedPatch jsonpatch.Patch decodedPatch jsonpatch.Patch
Target *types.Selector `json:"target,omitempty" yaml:"target,omitempty"` Target types.PatchTarget `json:"target,omitempty" yaml:"target,omitempty"`
Path string `json:"path,omitempty" yaml:"path,omitempty"` Path string `json:"path,omitempty" yaml:"path,omitempty"`
JsonOp string `json:"jsonOp,omitempty" yaml:"jsonOp,omitempty"` JsonOp string `json:"jsonOp,omitempty" yaml:"jsonOp,omitempty"`
} }
func (p *PatchJson6902TransformerPlugin) Config( func (p *PatchJson6902TransformerPlugin) Config(
@@ -71,33 +72,22 @@ func (p *PatchJson6902TransformerPlugin) Config(
} }
func (p *PatchJson6902TransformerPlugin) Transform(m resmap.ResMap) error { func (p *PatchJson6902TransformerPlugin) Transform(m resmap.ResMap) error {
if p.Target == nil { id := resid.NewResIdWithNamespace(
return fmt.Errorf("must specify a target for patch %s", p.JsonOp) resid.Gvk{
} Group: p.Target.Group,
resources, err := m.Select(*p.Target) Version: p.Target.Version,
Kind: p.Target.Kind,
},
p.Target.Name,
p.Target.Namespace,
)
obj, err := m.GetById(id)
if err != nil { if err != nil {
return err return err
} }
for _, res := range resources { return filtersutil.ApplyToJSON(patchjson6902.Filter{
internalAnnotations := kioutil.GetInternalAnnotations(&res.RNode) Patch: p.JsonOp,
}, obj)
err = res.ApplyFilter(patchjson6902.Filter{
Patch: p.JsonOp,
})
if err != nil {
return err
}
annotations := res.GetAnnotations()
for key, value := range internalAnnotations {
annotations[key] = value
}
err = res.SetAnnotations(annotations)
if err != nil {
return err
}
}
return nil
} }
func NewPatchJson6902TransformerPlugin() resmap.TransformerPlugin { func NewPatchJson6902TransformerPlugin() resmap.TransformerPlugin {

View File

@@ -0,0 +1,120 @@
// Code generated by pluginator on PatchStrategicMergeTransformer; DO NOT EDIT.
// pluginator {unknown 1970-01-01T00:00:00Z }
package builtins
import (
"fmt"
"strings"
"github.com/pkg/errors"
"sigs.k8s.io/kustomize/api/filters/patchstrategicmerge"
"sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/resource"
"sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/filtersutil"
"sigs.k8s.io/yaml"
)
type PatchStrategicMergeTransformerPlugin struct {
h *resmap.PluginHelpers
loadedPatches []*resource.Resource
Paths []types.PatchStrategicMerge `json:"paths,omitempty" yaml:"paths,omitempty"`
Patches string `json:"patches,omitempty" yaml:"patches,omitempty"`
}
func (p *PatchStrategicMergeTransformerPlugin) Config(
h *resmap.PluginHelpers, c []byte) (err error) {
p.h = h
err = yaml.Unmarshal(c, p)
if err != nil {
return err
}
if len(p.Paths) == 0 && p.Patches == "" {
return fmt.Errorf("empty file path and empty patch content")
}
if len(p.Paths) != 0 {
for _, onePath := range p.Paths {
res, err := p.h.ResmapFactory().RF().SliceFromBytes([]byte(onePath))
if err == nil {
p.loadedPatches = append(p.loadedPatches, res...)
continue
}
res, err = p.h.ResmapFactory().RF().SliceFromPatches(
p.h.Loader(), []types.PatchStrategicMerge{onePath})
if err != nil {
return err
}
p.loadedPatches = append(p.loadedPatches, res...)
}
}
if p.Patches != "" {
res, err := p.h.ResmapFactory().RF().SliceFromBytes([]byte(p.Patches))
if err != nil {
return err
}
p.loadedPatches = append(p.loadedPatches, res...)
}
if len(p.loadedPatches) == 0 {
return fmt.Errorf(
"patch appears to be empty; files=%v, Patch=%s", p.Paths, p.Patches)
}
return err
}
func (p *PatchStrategicMergeTransformerPlugin) Transform(m resmap.ResMap) error {
patches, err := p.h.ResmapFactory().Merge(p.loadedPatches)
if err != nil {
return err
}
for _, patch := range patches.Resources() {
target, err := m.GetById(patch.OrgId())
if err != nil {
return err
}
patchCopy := patch.DeepCopy()
patchCopy.SetName(target.GetName())
patchCopy.SetNamespace(target.GetNamespace())
patchCopy.SetGvk(target.GetGvk())
node, err := filtersutil.GetRNode(patchCopy)
if err != nil {
return err
}
err = filtersutil.ApplyToJSON(patchstrategicmerge.Filter{
Patch: node,
}, target)
if err != nil {
// Check for an error string from UnmarshalJSON that's indicative
// of an object that's missing basic KRM fields, and thus may have been
// entirely deleted (an acceptable outcome). This error handling should
// be deleted along with use of ResMap and apimachinery functions like
// UnmarshalJSON.
if !strings.Contains(err.Error(), "Object 'Kind' is missing") {
// Some unknown error, let it through.
return err
}
if len(target.Map()) != 0 {
return errors.Wrapf(
err, "with unexpectedly non-empty object map of size %d",
len(target.Map()))
}
// Fall through to handle deleted object.
}
if len(target.Map()) == 0 {
// This means all fields have been removed from the object.
// This can happen if a patch required deletion of the
// entire resource (not just a part of it). This means
// the overall resmap must shrink by one.
err = m.Remove(target.CurId())
if err != nil {
return err
}
}
}
return nil
}
func NewPatchStrategicMergeTransformerPlugin() resmap.TransformerPlugin {
return &PatchStrategicMergeTransformerPlugin{}
}

View File

@@ -9,10 +9,11 @@ import (
jsonpatch "github.com/evanphx/json-patch" jsonpatch "github.com/evanphx/json-patch"
"sigs.k8s.io/kustomize/api/filters/patchjson6902" "sigs.k8s.io/kustomize/api/filters/patchjson6902"
"sigs.k8s.io/kustomize/api/filters/patchstrategicmerge"
"sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/resource" "sigs.k8s.io/kustomize/api/resource"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/kio/kioutil" "sigs.k8s.io/kustomize/kyaml/filtersutil"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
@@ -22,7 +23,6 @@ type PatchTransformerPlugin struct {
Path string `json:"path,omitempty" yaml:"path,omitempty"` Path string `json:"path,omitempty" yaml:"path,omitempty"`
Patch string `json:"patch,omitempty" yaml:"patch,omitempty"` Patch string `json:"patch,omitempty" yaml:"patch,omitempty"`
Target *types.Selector `json:"target,omitempty" yaml:"target,omitempty"` Target *types.Selector `json:"target,omitempty" yaml:"target,omitempty"`
Options map[string]bool `json:"options,omitempty" yaml:"options,omitempty"`
} }
func (p *PatchTransformerPlugin) Config( func (p *PatchTransformerPlugin) Config(
@@ -40,6 +40,7 @@ func (p *PatchTransformerPlugin) Config(
return fmt.Errorf( return fmt.Errorf(
"patch and path can't be set at the same time\n%s", string(c)) "patch and path can't be set at the same time\n%s", string(c))
} }
if p.Path != "" { if p.Path != "" {
loaded, loadErr := h.Loader().Load(p.Path) loaded, loadErr := h.Loader().Load(p.Path)
if loadErr != nil { if loadErr != nil {
@@ -62,12 +63,6 @@ func (p *PatchTransformerPlugin) Config(
} }
if errSM == nil { if errSM == nil {
p.loadedPatch = patchSM p.loadedPatch = patchSM
if p.Options["allowNameChange"] {
p.loadedPatch.AllowNameChange()
}
if p.Options["allowKindChange"] {
p.loadedPatch.AllowKindChange()
}
} else { } else {
p.decodedPatch = patchJson p.decodedPatch = patchJson
} }
@@ -77,9 +72,10 @@ func (p *PatchTransformerPlugin) Config(
func (p *PatchTransformerPlugin) Transform(m resmap.ResMap) error { func (p *PatchTransformerPlugin) Transform(m resmap.ResMap) error {
if p.loadedPatch == nil { if p.loadedPatch == nil {
return p.transformJson6902(m, p.decodedPatch) return p.transformJson6902(m, p.decodedPatch)
} else {
// The patch was a strategic merge patch
return p.transformStrategicMerge(m, p.loadedPatch)
} }
// The patch was a strategic merge patch
return p.transformStrategicMerge(m, p.loadedPatch)
} }
// transformStrategicMerge applies the provided strategic merge patch // transformStrategicMerge applies the provided strategic merge patch
@@ -91,13 +87,36 @@ func (p *PatchTransformerPlugin) transformStrategicMerge(m resmap.ResMap, patch
if err != nil { if err != nil {
return err return err
} }
return target.ApplySmPatch(patch) return p.applySMPatch(target, patch)
} }
selected, err := m.Select(*p.Target)
resources, err := m.Select(*p.Target)
if err != nil { if err != nil {
return err return err
} }
return m.ApplySmPatch(resource.MakeIdSet(selected), patch) for _, res := range resources {
patchCopy := patch.DeepCopy()
patchCopy.SetName(res.GetName())
patchCopy.SetNamespace(res.GetNamespace())
patchCopy.SetGvk(res.GetGvk())
err := p.applySMPatch(res, patchCopy)
if err != nil {
return err
}
}
return nil
}
// applySMPatch applies the provided strategic merge patch to the
// given resource.
func (p *PatchTransformerPlugin) applySMPatch(resource, patch *resource.Resource) error {
node, err := filtersutil.GetRNode(patch)
if err != nil {
return err
}
return filtersutil.ApplyToJSON(patchstrategicmerge.Filter{
Patch: node,
}, resource)
} }
// transformJson6902 applies the provided json6902 patch // transformJson6902 applies the provided json6902 patch
@@ -111,20 +130,12 @@ func (p *PatchTransformerPlugin) transformJson6902(m resmap.ResMap, patch jsonpa
return err return err
} }
for _, res := range resources { for _, res := range resources {
res.StorePreviousId() err = filtersutil.ApplyToJSON(patchjson6902.Filter{
internalAnnotations := kioutil.GetInternalAnnotations(&res.RNode)
err = res.ApplyFilter(patchjson6902.Filter{
Patch: p.Patch, Patch: p.Patch,
}) }, res)
if err != nil { if err != nil {
return err return err
} }
annotations := res.GetAnnotations()
for key, value := range internalAnnotations {
annotations[key] = value
}
err = res.SetAnnotations(annotations)
} }
return nil return nil
} }

View File

@@ -0,0 +1,101 @@
// Code generated by pluginator on PrefixSuffixTransformer; DO NOT EDIT.
// pluginator {unknown 1970-01-01T00:00:00Z }
package builtins
import (
"errors"
"sigs.k8s.io/kustomize/api/filters/prefixsuffix"
"sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/filtersutil"
"sigs.k8s.io/yaml"
)
// Add the given prefix and suffix to the field.
type PrefixSuffixTransformerPlugin struct {
Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"`
Suffix string `json:"suffix,omitempty" yaml:"suffix,omitempty"`
FieldSpecs types.FsSlice `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"`
}
// A Gvk skip list for prefix/suffix modification.
// hard coded for now - eventually should be part of config.
var prefixSuffixFieldSpecsToSkip = types.FsSlice{
{Gvk: resid.Gvk{Kind: "CustomResourceDefinition"}},
{Gvk: resid.Gvk{Group: "apiregistration.k8s.io", Kind: "APIService"}},
{Gvk: resid.Gvk{Kind: "Namespace"}},
}
func (p *PrefixSuffixTransformerPlugin) Config(
_ *resmap.PluginHelpers, c []byte) (err error) {
p.Prefix = ""
p.Suffix = ""
p.FieldSpecs = nil
err = yaml.Unmarshal(c, p)
if err != nil {
return
}
if p.FieldSpecs == nil {
return errors.New("fieldSpecs is not expected to be nil")
}
return
}
func (p *PrefixSuffixTransformerPlugin) Transform(m resmap.ResMap) error {
// 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() {
// TODO: move this test into the filter (i.e. make a better filter)
if p.shouldSkip(r.OrgId()) {
continue
}
id := r.OrgId()
// current default configuration contains
// only one entry: "metadata/name" with no GVK
for _, fs := range p.FieldSpecs {
// TODO: this is redundant to filter (but needed for now)
if !id.IsSelected(&fs.Gvk) {
continue
}
// TODO: move this test into the filter.
if smellsLikeANameChange(&fs) {
// "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)
}
err := filtersutil.ApplyToJSON(prefixsuffix.Filter{
Prefix: p.Prefix,
Suffix: p.Suffix,
FieldSpec: fs,
}, r)
if err != nil {
return err
}
}
}
return nil
}
func smellsLikeANameChange(fs *types.FieldSpec) bool {
return fs.Path == "metadata/name"
}
func (p *PrefixSuffixTransformerPlugin) shouldSkip(id resid.ResId) bool {
for _, path := range prefixSuffixFieldSpecsToSkip {
if id.IsSelected(&path.Gvk) {
return true
}
}
return false
}
func NewPrefixSuffixTransformerPlugin() resmap.TransformerPlugin {
return &PrefixSuffixTransformerPlugin{}
}

View File

@@ -7,9 +7,11 @@ import (
"fmt" "fmt"
"sigs.k8s.io/kustomize/api/filters/replicacount" "sigs.k8s.io/kustomize/api/filters/replicacount"
"sigs.k8s.io/kustomize/kyaml/filtersutil"
"sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/resid"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
@@ -31,17 +33,19 @@ func (p *ReplicaCountTransformerPlugin) Transform(m resmap.ResMap) error {
found := false found := false
for _, fs := range p.FieldSpecs { for _, fs := range p.FieldSpecs {
matcher := p.createMatcher(fs) matcher := p.createMatcher(fs)
resList := m.GetMatchingResourcesByAnyId(matcher) matchOriginal := m.GetMatchingResourcesByOriginalId(matcher)
resList := append(
matchOriginal, m.GetMatchingResourcesByCurrentId(matcher)...)
if len(resList) > 0 { if len(resList) > 0 {
found = true found = true
for _, r := range resList { for _, r := range resList {
// There are redundant checks in the filter // There are redundant checks in the filter
// that we'll live with until resolution of // that we'll live with until resolution of
// https://github.com/kubernetes-sigs/kustomize/issues/2506 // https://github.com/kubernetes-sigs/kustomize/issues/2506
err := r.ApplyFilter(replicacount.Filter{ err := filtersutil.ApplyToJSON(replicacount.Filter{
Replica: p.Replica, Replica: p.Replica,
FieldSpec: fs, FieldSpec: fs,
}) }, r)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -13,6 +13,7 @@ import (
"sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/resource" "sigs.k8s.io/kustomize/api/resource"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/filtersutil"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
@@ -118,15 +119,15 @@ func (p *ValueAddTransformerPlugin) Transform(m resmap.ResMap) (err error) {
// TODO: consider t.NotSelector if implemented // TODO: consider t.NotSelector if implemented
for _, res := range resources { for _, res := range resources {
if t.FieldPath == types.MetadataNamespacePath { if t.FieldPath == types.MetadataNamespacePath {
err = res.ApplyFilter(namespace.Filter{ err = filtersutil.ApplyToJSON(namespace.Filter{
Namespace: p.Value, Namespace: p.Value,
}) }, res)
} else { } else {
err = res.ApplyFilter(valueadd.Filter{ err = filtersutil.ApplyToJSON(valueadd.Filter{
Value: p.Value, Value: p.Value,
FieldPath: t.FieldPath, FieldPath: t.FieldPath,
FilePathPosition: t.FilePathPosition, FilePathPosition: t.FilePathPosition,
}) }, res)
} }
if err != nil { if err != nil {
return err return err

View File

@@ -1,51 +0,0 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Deprecated: Package api/builtins will not be available in API v1.
package builtins
import (
internal "sigs.k8s.io/kustomize/api/internal/builtins"
)
type (
AnnotationsTransformerPlugin = internal.AnnotationsTransformerPlugin
ConfigMapGeneratorPlugin = internal.ConfigMapGeneratorPlugin
HashTransformerPlugin = internal.HashTransformerPlugin
HelmChartInflationGeneratorPlugin = internal.HelmChartInflationGeneratorPlugin
IAMPolicyGeneratorPlugin = internal.IAMPolicyGeneratorPlugin
ImageTagTransformerPlugin = internal.ImageTagTransformerPlugin
LabelTransformerPlugin = internal.LabelTransformerPlugin
LegacyOrderTransformerPlugin = internal.LegacyOrderTransformerPlugin
NamespaceTransformerPlugin = internal.NamespaceTransformerPlugin
PatchJson6902TransformerPlugin = internal.PatchJson6902TransformerPlugin
PatchStrategicMergeTransformerPlugin = internal.PatchStrategicMergeTransformerPlugin
PatchTransformerPlugin = internal.PatchTransformerPlugin
PrefixTransformerPlugin = internal.PrefixTransformerPlugin
SuffixTransformerPlugin = internal.SuffixTransformerPlugin
ReplacementTransformerPlugin = internal.ReplacementTransformerPlugin
ReplicaCountTransformerPlugin = internal.ReplicaCountTransformerPlugin
SecretGeneratorPlugin = internal.SecretGeneratorPlugin
ValueAddTransformerPlugin = internal.ValueAddTransformerPlugin
)
var (
NewAnnotationsTransformerPlugin = internal.NewAnnotationsTransformerPlugin
NewConfigMapGeneratorPlugin = internal.NewConfigMapGeneratorPlugin
NewHashTransformerPlugin = internal.NewHashTransformerPlugin
NewHelmChartInflationGeneratorPlugin = internal.NewHelmChartInflationGeneratorPlugin
NewIAMPolicyGeneratorPlugin = internal.NewIAMPolicyGeneratorPlugin
NewImageTagTransformerPlugin = internal.NewImageTagTransformerPlugin
NewLabelTransformerPlugin = internal.NewLabelTransformerPlugin
NewLegacyOrderTransformerPlugin = internal.NewLegacyOrderTransformerPlugin
NewNamespaceTransformerPlugin = internal.NewNamespaceTransformerPlugin
NewPatchJson6902TransformerPlugin = internal.NewPatchJson6902TransformerPlugin
NewPatchStrategicMergeTransformerPlugin = internal.NewPatchStrategicMergeTransformerPlugin
NewPatchTransformerPlugin = internal.NewPatchTransformerPlugin
NewPrefixTransformerPlugin = internal.NewPrefixTransformerPlugin
NewSuffixTransformerPlugin = internal.NewSuffixTransformerPlugin
NewReplacementTransformerPlugin = internal.NewReplacementTransformerPlugin
NewReplicaCountTransformerPlugin = internal.NewReplicaCountTransformerPlugin
NewSecretGeneratorPlugin = internal.NewSecretGeneratorPlugin
NewValueAddTransformerPlugin = internal.NewValueAddTransformerPlugin
)

View File

@@ -1,15 +1,13 @@
// Copyright 2019 The Kubernetes Authors. // Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
//go:build !windows package filesys_test
// +build !windows
package filesys
import ( import (
"os"
"path/filepath" "path/filepath"
"testing" "testing"
. "sigs.k8s.io/kustomize/api/filesys"
) )
func TestJoin(t *testing.T) { func TestJoin(t *testing.T) {
@@ -100,7 +98,6 @@ func TestNewTempConfirmDir(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
defer os.RemoveAll(string(tmp))
delinked, err := filepath.EvalSymlinks(string(tmp)) delinked, err := filepath.EvalSymlinks(string(tmp))
if err != nil { if err != nil {

View File

@@ -1,61 +0,0 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package filesys provides a file system abstraction,
// a subset of that provided by golang.org/pkg/os,
// with an on-disk and in-memory representation.
//
// Deprecated: use sigs.k8s.io/kustomize/kyaml/filesys instead.
package filesys
import "sigs.k8s.io/kustomize/kyaml/filesys"
const (
// Separator is deprecated, use sigs.k8s.io/kustomize/kyaml/filesys.Separator.
Separator = filesys.Separator
// SelfDir is deprecated, use sigs.k8s.io/kustomize/kyaml/filesys.SelfDir.
SelfDir = filesys.SelfDir
// ParentDir is deprecated, use sigs.k8s.io/kustomize/kyaml/filesys.ParentDir.
ParentDir = filesys.ParentDir
)
type (
// FileSystem is deprecated, use sigs.k8s.io/kustomize/kyaml/filesys.FileSystem.
FileSystem = filesys.FileSystem
// FileSystemOrOnDisk is deprecated, use sigs.k8s.io/kustomize/kyaml/filesys.FileSystemOrOnDisk.
FileSystemOrOnDisk = filesys.FileSystemOrOnDisk
// ConfirmedDir is deprecated, use sigs.k8s.io/kustomize/kyaml/filesys.ConfirmedDir.
ConfirmedDir = filesys.ConfirmedDir
)
// MakeEmptyDirInMemory is deprecated, use sigs.k8s.io/kustomize/kyaml/filesys.MakeEmptyDirInMemory.
func MakeEmptyDirInMemory() FileSystem { return filesys.MakeEmptyDirInMemory() }
// MakeFsInMemory is deprecated, use sigs.k8s.io/kustomize/kyaml/filesys.MakeFsInMemory.
func MakeFsInMemory() FileSystem { return filesys.MakeFsInMemory() }
// MakeFsOnDisk is deprecated, use sigs.k8s.io/kustomize/kyaml/filesys.MakeFsOnDisk.
func MakeFsOnDisk() FileSystem { return filesys.MakeFsOnDisk() }
// NewTmpConfirmedDir is deprecated, use sigs.k8s.io/kustomize/kyaml/filesys.NewTmpConfirmedDir.
func NewTmpConfirmedDir() (filesys.ConfirmedDir, error) { return filesys.NewTmpConfirmedDir() }
// RootedPath is deprecated, use sigs.k8s.io/kustomize/kyaml/filesys.RootedPath.
func RootedPath(elem ...string) string { return filesys.RootedPath(elem...) }
// StripTrailingSeps is deprecated, use sigs.k8s.io/kustomize/kyaml/filesys.StripTrailingSeps.
func StripTrailingSeps(s string) string { return filesys.StripTrailingSeps(s) }
// StripLeadingSeps is deprecated, use sigs.k8s.io/kustomize/kyaml/filesys.StripLeadingSeps.
func StripLeadingSeps(s string) string { return filesys.StripLeadingSeps(s) }
// PathSplit is deprecated, use sigs.k8s.io/kustomize/kyaml/filesys.PathSplit.
func PathSplit(incoming string) []string { return filesys.PathSplit(incoming) }
// PathJoin is deprecated, use sigs.k8s.io/kustomize/kyaml/filesys.PathJoin.
func PathJoin(incoming []string) string { return filesys.PathJoin(incoming) }
// InsertPathPart is deprecated, use sigs.k8s.io/kustomize/kyaml/filesys.InsertPathPart.
func InsertPathPart(path string, pos int, part string) string {
return filesys.InsertPathPart(path, pos, part)
}

50
api/filesys/filesystem.go Normal file
View File

@@ -0,0 +1,50 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package filesys provides a file system abstraction layer.
package filesys
import (
"path/filepath"
)
const (
Separator = string(filepath.Separator)
SelfDir = "."
ParentDir = ".."
)
// FileSystem groups basic os filesystem methods.
// It's supposed be functional subset of https://golang.org/pkg/os
type FileSystem interface {
// Create a file.
Create(path string) (File, error)
// MkDir makes a directory.
Mkdir(path string) error
// MkDirAll makes a directory path, creating intervening directories.
MkdirAll(path string) error
// RemoveAll removes path and any children it contains.
RemoveAll(path string) error
// Open opens the named file for reading.
Open(path string) (File, error)
// IsDir returns true if the path is a directory.
IsDir(path string) bool
// CleanedAbs converts the given path into a
// directory and a file name, where the directory
// is represented as a ConfirmedDir and all that implies.
// If the entire path is a directory, the file component
// is an empty string.
CleanedAbs(path string) (ConfirmedDir, string, error)
// Exists is true if the path exists in the file system.
Exists(path string) bool
// Glob returns the list of matching files,
// emulating https://golang.org/pkg/path/filepath/#Glob
Glob(pattern string) ([]string, error)
// ReadFile returns the contents of the file at the given path.
ReadFile(path string) ([]byte, error)
// WriteFile writes the data to a file at the given path,
// overwriting anything that's already there.
WriteFile(path string, data []byte) error
// Walk walks the file system with the given WalkFunc.
Walk(path string, walkFn filepath.WalkFunc) error
}

View File

@@ -6,7 +6,6 @@ package filesys
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
@@ -38,9 +37,9 @@ type fsNode struct {
// if this node is a file, this is the content. // if this node is a file, this is the content.
content []byte content []byte
// if offset is not nil the file is open and it tracks // if this node is a file, this tracks whether or
// the current file offset. // not it is "open".
offset *int open bool
} }
// MakeEmptyDirInMemory returns an empty directory. // MakeEmptyDirInMemory returns an empty directory.
@@ -120,14 +119,11 @@ func (n *fsNode) addFile(name string, c []byte) (result *fsNode, err error) {
result, ok := parent.dir[fileName] result, ok := parent.dir[fileName]
if ok { if ok {
// File already exists; overwrite it. // File already exists; overwrite it.
if result.offset != nil { result.content = c
return nil, fmt.Errorf("cannot add already opened file '%s'", n.Path())
}
result.content = append(result.content[:0], c...)
return result, nil return result, nil
} }
result = &fsNode{ result = &fsNode{
content: append([]byte(nil), c...), content: c,
parent: parent, parent: parent,
} }
parent.dir[fileName] = result parent.dir[fileName] = result
@@ -137,12 +133,7 @@ func (n *fsNode) addFile(name string, c []byte) (result *fsNode, err error) {
// Create implements FileSystem. // Create implements FileSystem.
// Create makes an empty file. // Create makes an empty file.
func (n *fsNode) Create(path string) (result File, err error) { func (n *fsNode) Create(path string) (result File, err error) {
f, err := n.AddFile(path, nil) return n.AddFile(path, []byte{})
if err != nil {
return f, err
}
f.offset = new(int)
return f, nil
} }
// WriteFile implements FileSystem. // WriteFile implements FileSystem.
@@ -163,7 +154,6 @@ func (n *fsNode) AddFile(
} }
func (n *fsNode) addDir(path string) (result *fsNode, err error) { func (n *fsNode) addDir(path string) (result *fsNode, err error) {
parent := n parent := n
dName, subDirName := mySplit(path) dName, subDirName := mySplit(path)
if dName != "" { if dName != "" {
@@ -236,7 +226,7 @@ func (n *fsNode) CleanedAbs(path string) (ConfirmedDir, string, error) {
return "", "", errors.Wrap(err, "unable to clean") return "", "", errors.Wrap(err, "unable to clean")
} }
if node == nil { if node == nil {
return "", "", notExistError(path) return "", "", fmt.Errorf("'%s' doesn't exist", path)
} }
if node.isNodeADir() { if node.isNodeADir() {
return ConfirmedDir(node.Path()), "", nil return ConfirmedDir(node.Path()), "", nil
@@ -309,8 +299,7 @@ func (n *fsNode) RemoveAll(path string) error {
return err return err
} }
if result == nil { if result == nil {
// If the path doesn't exist, no need to remove anything. return fmt.Errorf("cannot find '%s' to remove it", path)
return nil
} }
return result.Remove() return result.Remove()
} }
@@ -350,32 +339,6 @@ func (n *fsNode) IsDir(path string) bool {
return result.isNodeADir() return result.isNodeADir()
} }
// ReadDir implements FileSystem.
func (n *fsNode) ReadDir(path string) ([]string, error) {
if !n.Exists(path) {
return nil, notExistError(path)
}
if !n.IsDir(path) {
return nil, fmt.Errorf("%s is not a directory", path)
}
dir, err := n.Find(path)
if err != nil {
return nil, err
}
if dir == nil {
return nil, fmt.Errorf("could not find directory %s", path)
}
keys := make([]string, len(dir.dir))
i := 0
for k := range dir.dir {
keys[i] = k
i++
}
return keys, nil
}
// Size returns the size of the node. // Size returns the size of the node.
func (n *fsNode) Size() int64 { func (n *fsNode) Size() int64 {
if n.isNodeADir() { if n.isNodeADir() {
@@ -385,38 +348,22 @@ func (n *fsNode) Size() int64 {
} }
// Open implements FileSystem. // Open implements FileSystem.
// Open opens the node in read-write mode and sets the offset its start. // Open opens the node for reading (just marks it).
// Writing right after opening the file will replace the original content
// and move the offset forward, as with a file opened with O_RDWR | O_CREATE.
//
// As an example, let's consider a file with content "content":
// - open: sets offset to start, content is "content"
// - write "@": offset increases by one, the content is now "@ontent"
// - read the rest: since offset is 1, the read operation returns "ontent"
// - write "$": offset is at EOF, so "$" is appended and content is now "@ontent$"
// - read the rest: returns 0 bytes and EOF
// - close: the content is still "@ontent$"
func (n *fsNode) Open(path string) (File, error) { func (n *fsNode) Open(path string) (File, error) {
result, err := n.Find(path) result, err := n.Find(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if result == nil { if result == nil {
return nil, notExistError(path) return nil, fmt.Errorf("cannot find '%s' to open it", path)
} }
if result.offset != nil { result.open = true
return nil, fmt.Errorf("cannot open previously opened file '%s'", path)
}
result.offset = new(int)
return result, nil return result, nil
} }
// Close marks the node closed. // Close marks the node closed.
func (n *fsNode) Close() error { func (n *fsNode) Close() error {
if n.offset == nil { n.open = false
return fmt.Errorf("cannot close already closed file '%s'", n.Path())
}
n.offset = nil
return nil return nil
} }
@@ -427,14 +374,11 @@ func (n *fsNode) ReadFile(path string) (c []byte, err error) {
return nil, err return nil, err
} }
if result == nil { if result == nil {
return nil, notExistError(path) return nil, fmt.Errorf("cannot find '%s' to read it", path)
}
if result.isNodeADir() {
return nil, fmt.Errorf("cannot read content from non-file '%s'", n.Path())
} }
c = make([]byte, len(result.content)) c = make([]byte, len(result.content))
copy(c, result.content) _, err = result.Read(c)
return c, nil return c, err
} }
// Read returns the content of the file node. // Read returns the content of the file node.
@@ -443,19 +387,7 @@ func (n *fsNode) Read(d []byte) (c int, err error) {
return 0, fmt.Errorf( return 0, fmt.Errorf(
"cannot read content from non-file '%s'", n.Path()) "cannot read content from non-file '%s'", n.Path())
} }
if n.offset == nil { return copy(d, n.content), nil
return 0, fmt.Errorf("cannot read from closed file '%s'", n.Path())
}
rest := n.content[*n.offset:]
if len(d) < len(rest) {
rest = rest[:len(d)]
} else {
err = io.EOF
}
copy(d, rest)
*n.offset += len(rest)
return len(rest), err
} }
// Write saves the contents of the argument to the file node. // Write saves the contents of the argument to the file node.
@@ -464,12 +396,8 @@ func (n *fsNode) Write(p []byte) (c int, err error) {
return 0, fmt.Errorf( return 0, fmt.Errorf(
"cannot write content to non-file '%s'", n.Path()) "cannot write content to non-file '%s'", n.Path())
} }
if n.offset == nil { n.content = make([]byte, len(p))
return 0, fmt.Errorf("cannot write to closed file '%s'", n.Path()) return copy(n.content, p), nil
}
n.content = append(n.content[:*n.offset], p...)
*n.offset = len(n.content)
return len(p), nil
} }
// ContentMatches returns true if v matches fake file's content. // ContentMatches returns true if v matches fake file's content.
@@ -494,7 +422,7 @@ func (n *fsNode) Walk(path string, walkFn filepath.WalkFunc) error {
return err return err
} }
if result == nil { if result == nil {
return notExistError(path) return fmt.Errorf("cannot find '%s' to walk it", path)
} }
return result.WalkMe(walkFn) return result.WalkMe(walkFn)
} }
@@ -587,7 +515,7 @@ func isLegalFileNameForCreation(n string) bool {
func (n *fsNode) RegExpGlob(pattern string) ([]string, error) { func (n *fsNode) RegExpGlob(pattern string) ([]string, error) {
var result []string var result []string
var expression = regexp.MustCompile(pattern) var expression = regexp.MustCompile(pattern)
err := n.WalkMe(func(path string, info os.FileInfo, err error) error { n.WalkMe(func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
} }
@@ -598,9 +526,6 @@ func (n *fsNode) RegExpGlob(pattern string) ([]string, error) {
} }
return nil return nil
}) })
if err != nil {
return nil, err
}
sort.Strings(result) sort.Strings(result)
return result, nil return result, nil
} }
@@ -612,8 +537,7 @@ func (n *fsNode) RegExpGlob(pattern string) ([]string, error) {
// This is how /bin/ls behaves. // This is how /bin/ls behaves.
func (n *fsNode) Glob(pattern string) ([]string, error) { func (n *fsNode) Glob(pattern string) ([]string, error) {
var result []string var result []string
var allFiles []string n.WalkMe(func(path string, info os.FileInfo, err error) error {
err := n.WalkMe(func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
} }
@@ -623,26 +547,11 @@ func (n *fsNode) Glob(pattern string) ([]string, error) {
return err return err
} }
if match { if match {
allFiles = append(allFiles, path) result = append(result, path)
} }
} }
return nil return nil
}) })
if err != nil {
return nil, err
}
if IsHiddenFilePath(pattern) {
result = allFiles
} else {
result = RemoveHiddenFiles(allFiles)
}
sort.Strings(result) sort.Strings(result)
return result, nil return result, nil
} }
// notExistError indicates that a file or directory does not exist.
// Unwrapping returns os.ErrNotExist so errors.Is(err, os.ErrNotExist) works correctly.
type notExistError string
func (err notExistError) Error() string { return fmt.Sprintf("'%s' doesn't exist", string(err)) }
func (err notExistError) Unwrap() error { return os.ErrNotExist }

View File

@@ -1,16 +1,10 @@
// Copyright 2019 The Kubernetes Authors. // Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
//go:build !windows
// +build !windows
package filesys package filesys
import ( import (
"fmt" "fmt"
"io"
"io/ioutil"
"math/rand"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
@@ -97,6 +91,7 @@ func TestMakeFsInMemory(t *testing.T) {
func runBasicOperations( func runBasicOperations(
t *testing.T, tName string, isFSysRooted bool, t *testing.T, tName string, isFSysRooted bool,
cases []pathCase, fSys FileSystem) { cases []pathCase, fSys FileSystem) {
buff := make([]byte, 500)
for _, c := range cases { for _, c := range cases {
err := fSys.WriteFile(c.arg, []byte(content)) err := fSys.WriteFile(c.arg, []byte(content))
if c.errStr != "" { if c.errStr != "" {
@@ -133,44 +128,26 @@ func runBasicOperations(
if fi.Name() != c.name { if fi.Name() != c.name {
t.Fatalf("%s; expected name '%s', got '%s'", c.what, c.name, fi.Name()) t.Fatalf("%s; expected name '%s', got '%s'", c.what, c.name, fi.Name())
} }
buff, err := ioutil.ReadAll(f) count, err := f.Read(buff)
if err != nil { if err != nil {
t.Fatalf("%s; unexpected error: %v", c.what, err) t.Fatalf("%s; unexpected error: %v", c.what, err)
} }
if string(buff) != content { if string(buff[:count]) != content {
t.Fatalf("%s; unexpected buff '%s'", c.what, buff) t.Fatalf("%s; unexpected buff '%s'", c.what, buff)
} }
count, err := f.Write([]byte(shortContent)) count, err = f.Write([]byte(shortContent))
if err != nil { if err != nil {
t.Fatalf("%s; unexpected error: %v", c.what, err) t.Fatalf("%s; unexpected error: %v", c.what, err)
} }
if count != len(shortContent) { if count != len(shortContent) {
t.Fatalf("%s; unexpected count: %d", c.what, len(shortContent)) t.Fatalf("%s; unexpected count: %d", c.what, len(shortContent))
} }
if err := f.Close(); err != nil {
t.Fatalf("%s; unexpected error: %v", c.what, err)
}
stuff, err = fSys.ReadFile(c.path)
if err != nil {
t.Fatalf("%s; unexpected error: %v", c.what, err)
}
both := content + shortContent
if string(stuff) != both {
t.Fatalf("%s; unexpected content '%s', expected '%s'", c.what, stuff, both)
}
content := []byte(shortContent)
if err := fSys.WriteFile(c.path, content); err != nil {
t.Fatalf("%s; unexpected error: %v", c.what, err)
}
// This ensures that modifying the original slice does not change the contents of the file.
content[0] = '@'
stuff, err = fSys.ReadFile(c.path) stuff, err = fSys.ReadFile(c.path)
if err != nil { if err != nil {
t.Fatalf("%s; unexpected error: %v", c.what, err) t.Fatalf("%s; unexpected error: %v", c.what, err)
} }
if string(stuff) != shortContent { if string(stuff) != shortContent {
t.Fatalf("%s; unexpected content '%s', expected '%s'", c.what, stuff, shortContent) t.Fatalf("%s; unexpected content '%s'", c.what, stuff)
} }
} }
@@ -464,13 +441,6 @@ var bunchOfFiles = []struct {
{ {
path: filepath.Join("b", "d", "a", "c", "u"), path: filepath.Join("b", "d", "a", "c", "u"),
}, },
{
path: filepath.Join("b", "d", ".hidden_file"),
},
{
path: filepath.Join("b", "d", ".hidden_dir"),
addAsDir: true,
},
} }
func makeLoadedFileTree(t *testing.T) *fsNode { func makeLoadedFileTree(t *testing.T) *fsNode {
@@ -587,7 +557,6 @@ func TestExists(t *testing.T) {
func TestRegExpGlob(t *testing.T) { func TestRegExpGlob(t *testing.T) {
n := makeLoadedFileTree(t) n := makeLoadedFileTree(t)
expected := []string{ expected := []string{
filepath.Join("b", "d", ".hidden_file"),
filepath.Join("b", "d", "a", "c", "i", "beans"), filepath.Join("b", "d", "a", "c", "i", "beans"),
filepath.Join("b", "d", "a", "c", "m"), filepath.Join("b", "d", "a", "c", "m"),
filepath.Join("b", "d", "a", "c", "u"), filepath.Join("b", "d", "a", "c", "u"),
@@ -607,40 +576,19 @@ func TestRegExpGlob(t *testing.T) {
func TestGlob(t *testing.T) { func TestGlob(t *testing.T) {
n := makeLoadedFileTree(t) n := makeLoadedFileTree(t)
expected := []string{
tests := map[string]struct { filepath.Join("b", "d", "x"),
globPattern string filepath.Join("b", "d", "y"),
expectedFiles []string filepath.Join("b", "d", "z"),
}{
"VisibleFiles": {
globPattern: "b/d/*",
expectedFiles: []string{
filepath.Join("b", "d", "x"),
filepath.Join("b", "d", "y"),
filepath.Join("b", "d", "z"),
},
},
"HiddenFiles": {
globPattern: "b/d/.*",
expectedFiles: []string{
filepath.Join("b", "d", ".hidden_file"),
},
},
} }
paths, err := n.Glob("b/d/*")
for test, c := range tests { if err != nil {
t.Run(test, func(t *testing.T) { t.Fatalf("glob error: %v", err)
paths, err := n.Glob(c.globPattern)
if err != nil {
t.Fatalf("glob error: %v", err)
}
assertEqualStringSlices(t, c.expectedFiles, paths, "glob test")
})
} }
assertEqualStringSlices(t, expected, paths, "glob test")
} }
func assertEqualStringSlices(t *testing.T, expected, actual []string, message string) { func assertEqualStringSlices(t *testing.T, expected, actual []string, message string) {
t.Helper()
if len(expected) != len(actual) { if len(expected) != len(actual) {
t.Fatalf( t.Fatalf(
"%s; unequal sizes; len(expected)=%d, len(actual)=%d\n%+v\n%+v\n", "%s; unequal sizes; len(expected)=%d, len(actual)=%d\n%+v\n%+v\n",
@@ -838,52 +786,3 @@ func TestCleanedAbs(t *testing.T) {
} }
} }
} }
func TestFileOps(t *testing.T) {
const path = "foo.txt"
content := strings.Repeat("longest content", 100)
fs := MakeFsInMemory()
f, err := fs.Create(path)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if _, err := fs.Open(path); err == nil {
t.Fatalf("expected already opened error, got nil")
}
if _, err := fmt.Fprint(f, content); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := f.Close(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := f.Close(); err == nil {
t.Fatalf("expected already closed error, got nil")
}
f, err = fs.Open(path)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer f.Close()
for {
buf := make([]byte, rand.Intn(10))
n, err := f.Read(buf)
if err != nil && err != io.EOF {
t.Fatalf("unexpected error: %v", err)
}
if content[:n] != string(buf[:n]) {
t.Fatalf("unexpected read: expected %q got %q", content[:n], buf[:n])
}
content = content[n:]
if err != io.EOF {
continue
}
if len(content) == 0 {
break
}
t.Fatalf("unexpected EOF: remaining %d bytes", len(content))
}
}

View File

@@ -57,7 +57,7 @@ func (x fsOnDisk) CleanedAbs(
deLinked, err := filepath.EvalSymlinks(absRoot) deLinked, err := filepath.EvalSymlinks(absRoot)
if err != nil { if err != nil {
return "", "", fmt.Errorf( return "", "", fmt.Errorf(
"evalsymlink failure on '%s' : %w", path, err) "evalsymlink failure on '%s' : %v", path, err)
} }
if x.IsDir(deLinked) { if x.IsDir(deLinked) {
return ConfirmedDir(deLinked), "", nil return ConfirmedDir(deLinked), "", nil
@@ -88,17 +88,7 @@ func (fsOnDisk) Exists(name string) bool {
// Glob returns the list of matching files // Glob returns the list of matching files
func (fsOnDisk) Glob(pattern string) ([]string, error) { func (fsOnDisk) Glob(pattern string) ([]string, error) {
var result []string return filepath.Glob(pattern)
allFilePaths, err := filepath.Glob(pattern)
if err != nil {
return nil, err
}
if IsHiddenFilePath(pattern) {
result = allFilePaths
} else {
result = RemoveHiddenFiles(allFilePaths)
}
return result, nil
} }
// IsDir delegates to os.Stat and FileInfo.IsDir // IsDir delegates to os.Stat and FileInfo.IsDir
@@ -110,19 +100,6 @@ func (fsOnDisk) IsDir(name string) bool {
return info.IsDir() return info.IsDir()
} }
// ReadDir delegates to os.ReadDir
func (fsOnDisk) ReadDir(name string) ([]string, error) {
dirEntries, err := os.ReadDir(name)
if err != nil {
return nil, err
}
result := make([]string, len(dirEntries))
for i := range dirEntries {
result[i] = dirEntries[i].Name()
}
return result, nil
}
// ReadFile delegates to ioutil.ReadFile. // ReadFile delegates to ioutil.ReadFile.
func (fsOnDisk) ReadFile(name string) ([]byte, error) { return ioutil.ReadFile(name) } func (fsOnDisk) ReadFile(name string) ([]byte, error) { return ioutil.ReadFile(name) }

View File

@@ -1,10 +1,7 @@
// Copyright 2019 The Kubernetes Authors. // Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
//go:build !windows package filesys_test
// +build !windows
package filesys
import ( import (
"io/ioutil" "io/ioutil"
@@ -12,8 +9,9 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"reflect" "reflect"
"sort"
"testing" "testing"
. "sigs.k8s.io/kustomize/api/filesys"
) )
func makeTestDir(t *testing.T) (FileSystem, string) { func makeTestDir(t *testing.T) (FileSystem, string) {
@@ -137,113 +135,31 @@ func TestReadFilesRealFS(t *testing.T) {
fSys, testDir := makeTestDir(t) fSys, testDir := makeTestDir(t)
defer os.RemoveAll(testDir) defer os.RemoveAll(testDir)
dir := path.Join(testDir, "dir") err := fSys.WriteFile(path.Join(testDir, "foo"), []byte(`foo`))
nestedDir := path.Join(dir, "nestedDir")
hiddenDir := path.Join(testDir, ".hiddenDir")
dirs := []string{
testDir,
dir,
nestedDir,
hiddenDir,
}
// all directories will have all these files
files := []string{
"bar",
"foo",
"file-1.xtn",
".file-2.xtn",
".some-file-3.xtn",
".some-file-4.xtn",
}
err := fSys.MkdirAll(nestedDir)
if err != nil { if err != nil {
t.Fatalf("Unexpected Error %v\n", err) t.Fatalf("unexpected error %s", err)
} }
err = fSys.MkdirAll(hiddenDir) if !fSys.Exists(path.Join(testDir, "foo")) {
t.Fatalf("expected foo")
}
if fSys.IsDir(path.Join(testDir, "foo")) {
t.Fatalf("expected foo not to be a directory")
}
err = fSys.WriteFile(path.Join(testDir, "bar"), []byte(`bar`))
if err != nil { if err != nil {
t.Fatalf("Unexpected Error %v\n", err) t.Fatalf("unexpected error %s", err)
} }
// adding all files in every directory that we had defined files, err := fSys.Glob(path.Join("testDir", "*"))
for _, d := range dirs { expected := []string{
if !fSys.IsDir(d) { path.Join(testDir, "bar"),
t.Fatalf("Expected %s to be a dir\n", d) path.Join(testDir, "foo"),
}
for _, f := range files {
err = fSys.WriteFile(path.Join(d, f), []byte(f))
if err != nil {
t.Fatalf("unexpected error %s", err)
}
if !fSys.Exists(path.Join(d, f)) {
t.Fatalf("expected %s", f)
}
}
} }
if err != nil {
tests := map[string]struct { t.Fatalf("expected no error")
globPattern string
expectedFiles []string
expectedDirs map[string][]string // glob returns directories as well, so we need to add those to expected files
}{
"AllVisibleFiles": {
globPattern: "*",
expectedFiles: []string{
"bar",
"foo",
"file-1.xtn",
},
expectedDirs: map[string][]string{
testDir: []string{dir},
dir: []string{nestedDir},
},
},
"AllHiddenFiles": {
globPattern: ".*",
expectedFiles: []string{
".file-2.xtn",
".some-file-3.xtn",
".some-file-4.xtn",
},
expectedDirs: map[string][]string{
testDir: []string{hiddenDir},
},
},
"foo_File": {
globPattern: "foo",
expectedFiles: []string{
"foo",
},
},
"dotsome-file_PrefixedFiles": {
globPattern: ".some-file*",
expectedFiles: []string{
".some-file-3.xtn",
".some-file-4.xtn",
},
},
} }
if reflect.DeepEqual(files, expected) {
for n, c := range tests { t.Fatalf("incorrect files found by glob: %v", files)
t.Run(n, func(t *testing.T) {
for _, d := range dirs {
var expectedPaths []string
for _, f := range c.expectedFiles {
expectedPaths = append(expectedPaths, path.Join(d, f))
}
if c.expectedDirs != nil {
expectedPaths = append(expectedPaths, c.expectedDirs[d]...)
}
actualPaths, globErr := fSys.Glob(path.Join(d, c.globPattern))
if globErr != nil {
t.Fatalf("Unexpected Error : %v\n", globErr)
}
sort.Strings(actualPaths)
sort.Strings(expectedPaths)
if !reflect.DeepEqual(actualPaths, expectedPaths) {
t.Fatalf("incorrect files found by glob: expected=%v, actual=%v", expectedPaths, actualPaths)
}
}
})
} }
} }

View File

@@ -123,21 +123,3 @@ func InsertPathPart(path string, pos int, part string) string {
result[pos] = part result[pos] = part
return PathJoin(append(result, parts[pos:]...)) return PathJoin(append(result, parts[pos:]...))
} }
func IsHiddenFilePath(pattern string) bool {
return strings.HasPrefix(filepath.Base(pattern), ".")
}
// Removes paths containing hidden files/folders from a list of paths
func RemoveHiddenFiles(paths []string) []string {
if len(paths) == 0 {
return paths
}
var result []string
for _, path := range paths {
if !IsHiddenFilePath(path) {
result = append(result, path)
}
}
return result
}

View File

@@ -1,16 +1,11 @@
// Copyright 2021 The Kubernetes Authors. package filesys_test
// SPDX-License-Identifier: Apache-2.0
//go:build !windows
// +build !windows
package filesys
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"testing" "testing"
. "sigs.k8s.io/kustomize/api/filesys"
) )
// Confirm behavior of filepath.Match // Confirm behavior of filepath.Match
@@ -377,90 +372,3 @@ func TestStripLeadingSeps(t *testing.T) {
} }
} }
} }
func TestIsHiddenFilePath(t *testing.T) {
tests := map[string]struct {
paths []string
expectHidden bool
}{
"hiddenGlobs": {
expectHidden: true,
paths: []string{
".*",
"/.*",
"dir/.*",
"dir1/dir2/dir3/.*",
"../../.*",
"../../dir/.*",
},
},
"visibleGlobes": {
expectHidden: false,
paths: []string{
"*",
"/*",
"dir/*",
"dir1/dir2/dir3/*",
"../../*",
"../../dir/*",
},
},
"hiddenFiles": {
expectHidden: true,
paths: []string{
".root_file.xtn",
"/.file_1.xtn",
"dir/.file_2.xtn",
"dir1/dir2/dir3/.file_3.xtn",
"../../.file_4.xtn",
"../../dir/.file_5.xtn",
},
},
"visibleFiles": {
expectHidden: false,
paths: []string{
"root_file.xtn",
"/file_1.xtn",
"dir/file_2.xtn",
"dir1/dir2/dir3/file_3.xtn",
"../../file_4.xtn",
"../../dir/file_5.xtn",
},
},
}
for n, c := range tests {
t.Run(n, func(t *testing.T) {
for _, path := range c.paths {
actual := IsHiddenFilePath(path)
if actual != c.expectHidden {
t.Fatalf("For file path %q, expected hidden: %v, got hidden: %v", path, c.expectHidden, actual)
}
}
})
}
}
func TestRemoveHiddenFiles(t *testing.T) {
paths := []string{
"file1.xtn",
".file2.xtn",
"dir/fa1",
"dir/fa2",
"dir/.fa3",
"../../.fa4",
"../../fa5",
"../../dir/fa6",
"../../dir/.fa7",
}
result := RemoveHiddenFiles(paths)
expected := []string{
"file1.xtn",
"dir/fa1",
"dir/fa2",
"../../fa5",
"../../dir/fa6",
}
if !reflect.DeepEqual(result, expected) {
t.Fatalf("Hidden dirs not correctly removed, expected %v but got %v\n", expected, result)
}
}

View File

@@ -19,26 +19,18 @@ type Filter struct {
// FsSlice contains the FieldSpecs to locate the namespace field // FsSlice contains the FieldSpecs to locate the namespace field
FsSlice types.FsSlice FsSlice types.FsSlice
trackableSetter filtersutil.TrackableSetter
} }
var _ kio.Filter = Filter{} var _ kio.Filter = Filter{}
var _ kio.TrackableFilter = &Filter{}
// WithMutationTracker registers a callback which will be invoked each time a field is mutated
func (f *Filter) WithMutationTracker(callback func(key, value, tag string, node *yaml.RNode)) {
f.trackableSetter.WithMutationTracker(callback)
}
func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
keys := yaml.SortedMapKeys(f.Annotations) keys := filtersutil.SortedMapKeys(f.Annotations)
_, err := kio.FilterAll(yaml.FilterFunc( _, err := kio.FilterAll(yaml.FilterFunc(
func(node *yaml.RNode) (*yaml.RNode, error) { func(node *yaml.RNode) (*yaml.RNode, error) {
for _, k := range keys { for _, k := range keys {
if err := node.PipeE(fsslice.Filter{ if err := node.PipeE(fsslice.Filter{
FsSlice: f.FsSlice, FsSlice: f.FsSlice,
SetValue: f.trackableSetter.SetEntry( SetValue: filtersutil.SetEntry(
k, f.Annotations[k], yaml.NodeTagString), k, f.Annotations[k], yaml.NodeTagString),
CreateKind: yaml.MappingNode, // Annotations are MappingNodes. CreateKind: yaml.MappingNode, // Annotations are MappingNodes.
CreateTag: yaml.NodeTagMap, CreateTag: yaml.NodeTagMap,

View File

@@ -11,20 +11,16 @@ import (
"sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig" "sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig"
filtertest_test "sigs.k8s.io/kustomize/api/testutils/filtertest" filtertest_test "sigs.k8s.io/kustomize/api/testutils/filtertest"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/yaml"
) )
var annosFs = builtinconfig.MakeDefaultConfig().CommonAnnotations var annosFs = builtinconfig.MakeDefaultConfig().CommonAnnotations
func TestAnnotations_Filter(t *testing.T) { func TestAnnotations_Filter(t *testing.T) {
mutationTrackStub := filtertest_test.MutationTrackerStub{}
testCases := map[string]struct { testCases := map[string]struct {
input string input string
expectedOutput string expectedOutput string
filter Filter filter Filter
fsslice types.FsSlice fsslice types.FsSlice
setEntryCallback func(key, value, tag string, node *yaml.RNode)
expectedSetEntryArgs []filtertest_test.SetValueArg
}{ }{
"add": { "add": {
input: ` input: `
@@ -214,86 +210,17 @@ metadata:
"b": "b1", "b": "b1",
}}, }},
}, },
// test usage of SetEntryCallback
"set_entry_callback": {
input: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: instance
`,
expectedOutput: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: instance
annotations:
a: a1
b: b1
spec:
template:
metadata:
annotations:
a: a1
b: b1
`,
filter: Filter{
Annotations: annoMap{
"a": "a1",
"b": "b1",
},
},
setEntryCallback: mutationTrackStub.MutationTracker,
fsslice: []types.FieldSpec{
{
Path: "spec/template/metadata/annotations",
CreateIfNotPresent: true,
},
},
expectedSetEntryArgs: []filtertest_test.SetValueArg{
{
Key: "a",
Value: "a1",
Tag: "!!str",
NodePath: []string{"metadata", "annotations"},
},
{
Key: "a",
Value: "a1",
Tag: "!!str",
NodePath: []string{"spec", "template", "metadata", "annotations"},
},
{
Key: "b",
Value: "b1",
Tag: "!!str",
NodePath: []string{"metadata", "annotations"},
},
{
Key: "b",
Value: "b1",
Tag: "!!str",
NodePath: []string{"spec", "template", "metadata", "annotations"},
},
},
},
} }
for tn, tc := range testCases { for tn, tc := range testCases {
mutationTrackStub.Reset()
t.Run(tn, func(t *testing.T) { t.Run(tn, func(t *testing.T) {
filter := tc.filter filter := tc.filter
filter.WithMutationTracker(tc.setEntryCallback)
filter.FsSlice = append(annosFs, tc.fsslice...) filter.FsSlice = append(annosFs, tc.fsslice...)
if !assert.Equal(t, if !assert.Equal(t,
strings.TrimSpace(tc.expectedOutput), strings.TrimSpace(tc.expectedOutput),
strings.TrimSpace(filtertest_test.RunFilter(t, tc.input, filter))) { strings.TrimSpace(filtertest_test.RunFilter(t, tc.input, filter))) {
t.FailNow() t.FailNow()
} }
if !assert.Equal(t, tc.expectedSetEntryArgs, mutationTrackStub.SetValueArgs()) {
t.FailNow()
}
}) })
} }
} }

View File

@@ -28,9 +28,7 @@ metadata:
`)}}, `)}},
Filters: []kio.Filter{Filter{ Filters: []kio.Filter{Filter{
Annotations: map[string]string{ Annotations: map[string]string{
"foo": "bar", "foo": "bar",
"booleanValue": "true",
"numberValue": "42",
}, },
FsSlice: fss, FsSlice: fss,
}}, }},
@@ -46,16 +44,12 @@ metadata:
// metadata: // metadata:
// name: instance // name: instance
// annotations: // annotations:
// booleanValue: "true"
// foo: bar // foo: bar
// numberValue: "42"
// --- // ---
// apiVersion: example.com/v1 // apiVersion: example.com/v1
// kind: Bar // kind: Bar
// metadata: // metadata:
// name: instance // name: instance
// annotations: // annotations:
// booleanValue: "true"
// foo: bar // foo: bar
// numberValue: "42"
} }

View File

@@ -1,5 +0,0 @@
package filters
// Package filters collects various implementations
// sigs.k8s.io/kustomize/kyaml/kio.Filter used by kustomize
// transformers to modify kubernetes objects.

View File

@@ -4,29 +4,17 @@
package fieldspec package fieldspec
import ( import (
"fmt"
"strings" "strings"
"sigs.k8s.io/kustomize/api/filters/filtersutil" "sigs.k8s.io/kustomize/api/filters/filtersutil"
"sigs.k8s.io/kustomize/api/internal/utils"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/errors" "sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/resid"
"sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml"
) )
var _ yaml.Filter = Filter{} var _ yaml.Filter = Filter{}
// Filter possibly mutates its object argument using a FieldSpec. // Filter applies a single fieldSpec to a single object
// If the object matches the FieldSpec, and the node found
// by following the fieldSpec's path is non-null, this filter calls
// the setValue function on the node at the end of the path.
// If any part of the path doesn't exist, the filter returns
// without doing anything and without error, unless it was set
// to create the path. If set to create, it creates a tree of maps
// along the path, and the leaf node gets the setValue called on it.
// Error on GVK mismatch, empty or poorly formed path.
// Filter expect kustomize style paths, not JSON paths.
// Filter stores internal state and should not be reused // Filter stores internal state and should not be reused
type Filter struct { type Filter struct {
// FieldSpec contains the path to the value to set. // FieldSpec contains the path to the value to set.
@@ -46,52 +34,44 @@ type Filter struct {
func (fltr Filter) Filter(obj *yaml.RNode) (*yaml.RNode, error) { func (fltr Filter) Filter(obj *yaml.RNode) (*yaml.RNode, error) {
// check if the FieldSpec applies to the object // check if the FieldSpec applies to the object
if match := isMatchGVK(fltr.FieldSpec, obj); !match { if match, err := isMatchGVK(fltr.FieldSpec, obj); !match || err != nil {
return obj, nil return obj, errors.Wrap(err)
} }
fltr.path = utils.PathSplitter(fltr.FieldSpec.Path, "/") fltr.path = splitPath(fltr.FieldSpec.Path)
if err := fltr.filter(obj); err != nil { if err := fltr.filter(obj); err != nil {
s, _ := obj.String()
return nil, errors.WrapPrefixf(err, return nil, errors.WrapPrefixf(err,
"considering field '%s' of object %s", fltr.FieldSpec.Path, resid.FromRNode(obj)) "obj '%s' at path '%v'", s, fltr.FieldSpec.Path)
} }
return obj, nil return obj, nil
} }
// Recursively called.
func (fltr Filter) filter(obj *yaml.RNode) error { func (fltr Filter) filter(obj *yaml.RNode) error {
if len(fltr.path) == 0 { if len(fltr.path) == 0 {
// found the field -- set its value // found the field -- set its value
return fltr.SetValue(obj) return fltr.SetValue(obj)
} }
if obj.IsTaggedNull() || obj.IsNil() {
return nil
}
switch obj.YNode().Kind { switch obj.YNode().Kind {
case yaml.SequenceNode: case yaml.SequenceNode:
return fltr.handleSequence(obj) return fltr.seq(obj)
case yaml.MappingNode: case yaml.MappingNode:
return fltr.handleMap(obj) return fltr.field(obj)
case yaml.AliasNode:
return fltr.filter(yaml.NewRNode(obj.YNode().Alias))
default: default:
return errors.Errorf("expected sequence or mapping node") return errors.Errorf("expected sequence or mapping node")
} }
} }
// handleMap calls filter on the map field matching the next path element // field calls filter on the field matching the next path element
func (fltr Filter) handleMap(obj *yaml.RNode) error { func (fltr Filter) field(obj *yaml.RNode) error {
fieldName, isSeq := isSequenceField(fltr.path[0]) fieldName, isSeq := isSequenceField(fltr.path[0])
if fieldName == "" {
return fmt.Errorf("cannot set or create an empty field name")
}
// lookup the field matching the next path element // lookup the field matching the next path element
var operation yaml.Filter var lookupField yaml.Filter
var kind yaml.Kind var kind yaml.Kind
tag := yaml.NodeTagEmpty tag := "" // TODO: change to yaml.NodeTagEmpty
switch { switch {
case !fltr.FieldSpec.CreateIfNotPresent || fltr.CreateKind == 0 || isSeq: case !fltr.FieldSpec.CreateIfNotPresent || fltr.CreateKind == 0 || isSeq:
// don't create the field if we don't find it // dont' create the field if we don't find it
operation = yaml.Lookup(fieldName) lookupField = yaml.Lookup(fieldName)
if isSeq { if isSeq {
// The query path thinks this field should be a sequence; // The query path thinks this field should be a sequence;
// accept this hint for use later if the tag is NodeTagNull. // accept this hint for use later if the tag is NodeTagNull.
@@ -99,30 +79,25 @@ func (fltr Filter) handleMap(obj *yaml.RNode) error {
} }
case len(fltr.path) <= 1: case len(fltr.path) <= 1:
// create the field if it is missing: use the provided node kind // create the field if it is missing: use the provided node kind
operation = yaml.LookupCreate(fltr.CreateKind, fieldName) lookupField = yaml.LookupCreate(fltr.CreateKind, fieldName)
kind = fltr.CreateKind kind = fltr.CreateKind
tag = fltr.CreateTag tag = fltr.CreateTag
default: default:
// create the field if it is missing: must be a mapping node // create the field if it is missing: must be a mapping node
operation = yaml.LookupCreate(yaml.MappingNode, fieldName) lookupField = yaml.LookupCreate(yaml.MappingNode, fieldName)
kind = yaml.MappingNode kind = yaml.MappingNode
tag = yaml.NodeTagMap tag = yaml.NodeTagMap
} }
// locate (or maybe create) the field // locate (or maybe create) the field
field, err := obj.Pipe(operation) field, err := obj.Pipe(lookupField)
if err != nil { if err != nil || field == nil {
return errors.WrapPrefixf(err, "fieldName: %s", fieldName) return errors.WrapPrefixf(err, "fieldName: %s", fieldName)
} }
if field == nil {
// No error if field not found.
return nil
}
// if the value exists, but is null and kind is set, // if the value exists, but is null, then change it to the creation type
// then change it to the creation type
// TODO: update yaml.LookupCreate to support this // TODO: update yaml.LookupCreate to support this
if field.YNode().Tag == yaml.NodeTagNull && yaml.IsCreate(kind) { if field.YNode().Tag == yaml.NodeTagNull {
field.YNode().Kind = kind field.YNode().Kind = kind
field.YNode().Tag = tag field.YNode().Tag = tag
} }
@@ -135,10 +110,8 @@ func (fltr Filter) handleMap(obj *yaml.RNode) error {
} }
// seq calls filter on all sequence elements // seq calls filter on all sequence elements
func (fltr Filter) handleSequence(obj *yaml.RNode) error { func (fltr Filter) seq(obj *yaml.RNode) error {
if err := obj.VisitElements(func(node *yaml.RNode) error { if err := obj.VisitElements(func(node *yaml.RNode) error {
// set an accurate FieldPath for nested elements
node.AppendToFieldPath(obj.FieldPath()...)
// recurse on each element -- re-allocating a Filter is // recurse on each element -- re-allocating a Filter is
// not strictly required, but is more consistent with field // not strictly required, but is more consistent with field
// and less likely to have side effects // and less likely to have side effects
@@ -148,35 +121,56 @@ func (fltr Filter) handleSequence(obj *yaml.RNode) error {
return errors.WrapPrefixf(err, return errors.WrapPrefixf(err,
"visit traversal on path: %v", fltr.path) "visit traversal on path: %v", fltr.path)
} }
return nil return nil
} }
// isSequenceField returns true if the path element is for a sequence field. // isSequenceField returns true if the path element is for a sequence field.
// isSequence also returns the path element with the '[]' suffix trimmed // isSequence also returns the path element with the '[]' suffix trimmed
func isSequenceField(name string) (string, bool) { func isSequenceField(name string) (string, bool) {
shorter := strings.TrimSuffix(name, "[]") isSeq := strings.HasSuffix(name, "[]")
return shorter, shorter != name name = strings.TrimSuffix(name, "[]")
return name, isSeq
} }
// isMatchGVK returns true if the fs.GVK matches the obj GVK. // isMatchGVK returns true if the fs.GVK matches the obj GVK.
func isMatchGVK(fs types.FieldSpec, obj *yaml.RNode) bool { func isMatchGVK(fs types.FieldSpec, obj *yaml.RNode) (bool, error) {
if kind := obj.GetKind(); fs.Kind != "" && fs.Kind != kind { meta, err := obj.GetMeta()
if err != nil {
return false, err
}
if fs.Kind != "" && fs.Kind != meta.Kind {
// kind doesn't match // kind doesn't match
return false return false, err
} }
// parse the group and version from the apiVersion field // parse the group and version from the apiVersion field
group, version := resid.ParseGroupVersion(obj.GetApiVersion()) group, version := parseGV(meta.APIVersion)
if fs.Group != "" && fs.Group != group { if fs.Group != "" && fs.Group != group {
// group doesn't match // group doesn't match
return false return false, nil
} }
if fs.Version != "" && fs.Version != version { if fs.Version != "" && fs.Version != version {
// version doesn't match // version doesn't match
return false return false, nil
} }
return true return true, nil
}
func splitPath(path string) []string {
ps := strings.Split(path, "/")
var res []string
res = append(res, ps[0])
for i := 1; i < len(ps); i++ {
lastIndex := len(res) - 1
if strings.HasSuffix(res[lastIndex], "\\") {
res[lastIndex] = strings.TrimSuffix(res[lastIndex], "\\") + "/" + ps[i]
} else {
res = append(res, ps[i])
}
}
return res
} }

View File

@@ -15,239 +15,206 @@ import (
"sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml"
) )
func TestFilter_Filter(t *testing.T) { type TestCase struct {
testCases := map[string]struct { name string
input string input string
expected string expected string
filter fieldspec.Filter filter fieldspec.Filter
fieldSpec string fieldSpec string
error string error string
}{ }
"path not found": {
fieldSpec: `
path: a/b
group: foo
kind: Bar
`,
input: `
apiVersion: foo
kind: Bar
xxx:
`,
expected: `
apiVersion: foo
kind: Bar
xxx:
`,
filter: fieldspec.Filter{
SetValue: filtersutil.SetScalar("e"),
},
},
"empty path": {
fieldSpec: `
group: foo
version: v1
kind: Bar
`,
input: `
apiVersion: foo/v1
kind: Bar
xxx:
`,
expected: `
apiVersion: foo
kind: Bar
xxx:
`,
error: `considering field '' of object Bar.v1.foo/[noName].[noNs]: cannot set or create an empty field name`,
filter: fieldspec.Filter{
SetValue: filtersutil.SetScalar("e"),
},
},
"update": { var tests = []TestCase{
fieldSpec: ` {
name: "update",
fieldSpec: `
path: a/b path: a/b
group: foo group: foo
kind: Bar kind: Bar
`, `,
input: ` input: `
apiVersion: foo/v1beta1 apiVersion: foo/v1beta1
kind: Bar kind: Bar
a: a:
b: c b: c
`, `,
expected: ` expected: `
apiVersion: foo/v1beta1 apiVersion: foo/v1beta1
kind: Bar kind: Bar
a: a:
b: e b: e
`, `,
filter: fieldspec.Filter{ filter: fieldspec.Filter{
SetValue: filtersutil.SetScalar("e"), SetValue: filtersutil.SetScalar("e"),
},
}, },
},
"update-kind-not-match": { {
fieldSpec: ` name: "update-kind-not-match",
fieldSpec: `
path: a/b path: a/b
group: foo group: foo
kind: Bar1 kind: Bar1
`, `,
input: ` input: `
apiVersion: foo/v1beta1 apiVersion: foo/v1beta1
kind: Bar2 kind: Bar2
a: a:
b: c b: c
`, `,
expected: ` expected: `
apiVersion: foo/v1beta1 apiVersion: foo/v1beta1
kind: Bar2 kind: Bar2
a: a:
b: c b: c
`, `,
filter: fieldspec.Filter{ filter: fieldspec.Filter{
SetValue: filtersutil.SetScalar("e"), SetValue: filtersutil.SetScalar("e"),
},
}, },
},
"update-group-not-match": { {
fieldSpec: ` name: "update-group-not-match",
fieldSpec: `
path: a/b path: a/b
group: foo1 group: foo1
kind: Bar kind: Bar
`, `,
input: ` input: `
apiVersion: foo2/v1beta1 apiVersion: foo2/v1beta1
kind: Bar kind: Bar
a: a:
b: c b: c
`, `,
expected: ` expected: `
apiVersion: foo2/v1beta1 apiVersion: foo2/v1beta1
kind: Bar kind: Bar
a: a:
b: c b: c
`, `,
filter: fieldspec.Filter{ filter: fieldspec.Filter{
SetValue: filtersutil.SetScalar("e"), SetValue: filtersutil.SetScalar("e"),
},
}, },
},
"update-version-not-match": { {
fieldSpec: ` name: "update-version-not-match",
fieldSpec: `
path: a/b path: a/b
group: foo group: foo
version: v1beta1 version: v1beta1
kind: Bar kind: Bar
`, `,
input: ` input: `
apiVersion: foo/v1beta2 apiVersion: foo/v1beta2
kind: Bar kind: Bar
a: a:
b: c b: c
`, `,
expected: ` expected: `
apiVersion: foo/v1beta2 apiVersion: foo/v1beta2
kind: Bar kind: Bar
a: a:
b: c b: c
`, `,
filter: fieldspec.Filter{ filter: fieldspec.Filter{
SetValue: filtersutil.SetScalar("e"), SetValue: filtersutil.SetScalar("e"),
},
}, },
},
"bad-version": { {
fieldSpec: ` name: "bad-version",
fieldSpec: `
path: a/b path: a/b
group: foo group: foo
version: v1beta1 version: v1beta1
kind: Bar kind: Bar
`, `,
input: ` input: `
apiVersion: foo/v1beta2/something apiVersion: foo/v1beta2/something
kind: Bar kind: Bar
a: a:
b: c b: c
`, `,
expected: ` expected: `
apiVersion: foo/v1beta2/something apiVersion: foo/v1beta2/something
kind: Bar kind: Bar
a: a:
b: c b: c
`, `,
filter: fieldspec.Filter{ filter: fieldspec.Filter{
SetValue: filtersutil.SetScalar("e"), SetValue: filtersutil.SetScalar("e"),
},
}, },
},
"bad-meta": { {
fieldSpec: ` name: "bad-meta",
fieldSpec: `
path: a/b path: a/b
group: foo group: foo
version: v1beta1 version: v1beta1
kind: Bar kind: Bar
`, `,
input: ` input: `
a: a:
b: c b: c
`, `,
expected: ` filter: fieldspec.Filter{
a: SetValue: filtersutil.SetScalar("e"),
b: c
`,
filter: fieldspec.Filter{
SetValue: filtersutil.SetScalar("e"),
},
}, },
error: "missing Resource metadata",
},
"miss-match-type": { {
fieldSpec: ` name: "miss-match-type",
fieldSpec: `
path: a/b/c path: a/b/c
kind: Bar kind: Bar
`, `,
input: ` input: `
kind: Bar kind: Bar
a: a:
b: a b: a
`, `,
error: `considering field 'a/b/c' of object Bar.[noVer].[noGrp]/[noName].[noNs]: expected sequence or mapping node`, error: "obj 'kind: Bar\na:\n b: a\n' at path 'a/b/c': " +
filter: fieldspec.Filter{ "expected sequence or mapping node",
SetValue: filtersutil.SetScalar("e"), filter: fieldspec.Filter{
}, SetValue: filtersutil.SetScalar("e"),
}, },
},
"add": { {
fieldSpec: ` name: "add",
fieldSpec: `
path: a/b/c/d path: a/b/c/d
group: foo group: foo
create: true create: true
kind: Bar kind: Bar
`, `,
input: ` input: `
apiVersion: foo/v1beta1 apiVersion: foo/v1beta1
kind: Bar kind: Bar
a: {} a: {}
`, `,
expected: ` expected: `
apiVersion: foo/v1beta1 apiVersion: foo/v1beta1
kind: Bar kind: Bar
a: {b: {c: {d: e}}} a: {b: {c: {d: e}}}
`, `,
filter: fieldspec.Filter{ filter: fieldspec.Filter{
SetValue: filtersutil.SetScalar("e"), SetValue: filtersutil.SetScalar("e"),
CreateKind: yaml.ScalarNode, CreateKind: yaml.ScalarNode,
},
}, },
},
"update-in-sequence": { {
fieldSpec: ` name: "update-in-sequence",
fieldSpec: `
path: a/b[]/c/d path: a/b[]/c/d
group: foo group: foo
kind: Bar kind: Bar
`, `,
input: ` input: `
apiVersion: foo/v1beta1 apiVersion: foo/v1beta1
kind: Bar kind: Bar
a: a:
@@ -255,7 +222,7 @@ a:
- c: - c:
d: a d: a
`, `,
expected: ` expected: `
apiVersion: foo/v1beta1 apiVersion: foo/v1beta1
kind: Bar kind: Bar
a: a:
@@ -263,237 +230,245 @@ a:
- c: - c:
d: e d: e
`, `,
filter: fieldspec.Filter{ filter: fieldspec.Filter{
SetValue: filtersutil.SetScalar("e"), SetValue: filtersutil.SetScalar("e"),
},
}, },
},
// Don't create a sequence // Don't create a sequence
"empty-sequence-no-create": { {
fieldSpec: ` name: "empty-sequence-no-create",
fieldSpec: `
path: a/b[]/c/d path: a/b[]/c/d
group: foo group: foo
create: true create: true
kind: Bar kind: Bar
`, `,
input: ` input: `
apiVersion: foo/v1beta1 apiVersion: foo/v1beta1
kind: Bar kind: Bar
a: {} a: {}
`, `,
expected: ` expected: `
apiVersion: foo/v1beta1 apiVersion: foo/v1beta1
kind: Bar kind: Bar
a: {} a: {}
`, `,
filter: fieldspec.Filter{ filter: fieldspec.Filter{
SetValue: filtersutil.SetScalar("e"), SetValue: filtersutil.SetScalar("e"),
CreateKind: yaml.ScalarNode, CreateKind: yaml.ScalarNode,
},
}, },
},
// Create a new field for an element in a sequence // Create a new field for an element in a sequence
"empty-sequence-create": { {
fieldSpec: ` name: "empty-sequence-create",
fieldSpec: `
path: a/b[]/c/d path: a/b[]/c/d
group: foo group: foo
create: true create: true
kind: Bar kind: Bar
`, `,
input: ` input: `
apiVersion: foo/v1beta1 apiVersion: foo/v1beta1
kind: Bar kind: Bar
a: a:
b: b:
- c: {} - c: {}
`, `,
expected: ` expected: `
apiVersion: foo/v1beta1 apiVersion: foo/v1beta1
kind: Bar kind: Bar
a: a:
b: b:
- c: {d: e} - c: {d: e}
`, `,
filter: fieldspec.Filter{ filter: fieldspec.Filter{
SetValue: filtersutil.SetScalar("e"), SetValue: filtersutil.SetScalar("e"),
CreateKind: yaml.ScalarNode, CreateKind: yaml.ScalarNode,
},
}, },
},
"group v1": { {
fieldSpec: ` name: "group v1",
fieldSpec: `
path: a/b path: a/b
group: v1 group: v1
create: true create: true
kind: Bar kind: Bar
`, `,
input: ` input: `
apiVersion: v1 apiVersion: v1
kind: Bar kind: Bar
`, `,
expected: ` expected: `
apiVersion: v1 apiVersion: v1
kind: Bar kind: Bar
`, `,
filter: fieldspec.Filter{ filter: fieldspec.Filter{
SetValue: filtersutil.SetScalar("e"), SetValue: filtersutil.SetScalar("e"),
CreateKind: yaml.ScalarNode, CreateKind: yaml.ScalarNode,
},
}, },
},
"version v1": { {
fieldSpec: ` name: "version v1",
fieldSpec: `
path: a/b path: a/b
version: v1 version: v1
create: true create: true
kind: Bar kind: Bar
`, `,
input: ` input: `
apiVersion: v1 apiVersion: v1
kind: Bar kind: Bar
`, `,
expected: ` expected: `
apiVersion: v1 apiVersion: v1
kind: Bar kind: Bar
a: a:
b: e b: e
`, `,
filter: fieldspec.Filter{ filter: fieldspec.Filter{
SetValue: filtersutil.SetScalar("e"), SetValue: filtersutil.SetScalar("e"),
CreateKind: yaml.ScalarNode, CreateKind: yaml.ScalarNode,
},
}, },
},
"successfully set field on array entry no sequence hint": { {
fieldSpec: ` name: "successfully set field on array entry no sequence hint",
fieldSpec: `
path: spec/containers/image path: spec/containers/image
version: v1 version: v1
kind: Bar kind: Bar
`, `,
input: ` input: `
apiVersion: v1 apiVersion: v1
kind: Bar kind: Bar
spec: spec:
containers: containers:
- image: foo - image: foo
`, `,
expected: ` expected: `
apiVersion: v1 apiVersion: v1
kind: Bar kind: Bar
spec: spec:
containers: containers:
- image: bar - image: bar
`, `,
filter: fieldspec.Filter{ filter: fieldspec.Filter{
SetValue: filtersutil.SetScalar("bar"), SetValue: filtersutil.SetScalar("bar"),
CreateKind: yaml.ScalarNode, CreateKind: yaml.ScalarNode,
},
}, },
},
"successfully set field on array entry with sequence hint": { {
fieldSpec: ` name: "successfully set field on array entry with sequence hint",
fieldSpec: `
path: spec/containers[]/image path: spec/containers[]/image
version: v1 version: v1
kind: Bar kind: Bar
`, `,
input: ` input: `
apiVersion: v1 apiVersion: v1
kind: Bar kind: Bar
spec: spec:
containers: containers:
- image: foo - image: foo
`, `,
expected: ` expected: `
apiVersion: v1 apiVersion: v1
kind: Bar kind: Bar
spec: spec:
containers: containers:
- image: bar - image: bar
`, `,
filter: fieldspec.Filter{ filter: fieldspec.Filter{
SetValue: filtersutil.SetScalar("bar"), SetValue: filtersutil.SetScalar("bar"),
CreateKind: yaml.ScalarNode, CreateKind: yaml.ScalarNode,
},
}, },
"failure to set field on array entry with sequence hint in path": { },
fieldSpec: ` {
name: "failure to set field on array entry with sequence hint in path",
fieldSpec: `
path: spec/containers[]/image path: spec/containers[]/image
version: v1 version: v1
kind: Bar kind: Bar
`, `,
input: ` input: `
apiVersion: v1 apiVersion: v1
kind: Bar kind: Bar
spec: spec:
containers: containers:
`, `,
expected: ` expected: `
apiVersion: v1 apiVersion: v1
kind: Bar kind: Bar
spec: spec:
containers: [] containers: []
`, `,
filter: fieldspec.Filter{ filter: fieldspec.Filter{
SetValue: filtersutil.SetScalar("bar"), SetValue: filtersutil.SetScalar("bar"),
CreateKind: yaml.ScalarNode, CreateKind: yaml.ScalarNode,
},
}, },
},
"failure to set field on array entry, no sequence hint in path": { {
fieldSpec: ` name: "failure to set field on array entry, no sequence hint in path",
fieldSpec: `
path: spec/containers/image path: spec/containers/image
version: v1 version: v1
kind: Bar kind: Bar
`, `,
input: ` input: `
apiVersion: v1 apiVersion: v1
kind: Bar kind: Bar
spec: spec:
containers: containers:
`, `,
expected: ` expected: `
apiVersion: v1 apiVersion: v1
kind: Bar kind: Bar
spec: spec:
containers: containers:
`, `,
filter: fieldspec.Filter{ filter: fieldspec.Filter{
SetValue: filtersutil.SetScalar("bar"), SetValue: filtersutil.SetScalar("bar"),
CreateKind: yaml.ScalarNode, CreateKind: yaml.ScalarNode,
},
}, },
"fieldname with slash '/'": { error: "obj '' at path 'spec/containers/image': expected sequence or mapping node",
fieldSpec: ` },
{
name: "filedname with slash '/'",
fieldSpec: `
path: a/b\/c/d path: a/b\/c/d
version: v1 version: v1
kind: Bar kind: Bar
`, `,
input: ` input: `
apiVersion: v1 apiVersion: v1
kind: Bar kind: Bar
a: a:
b/c: b/c:
d: foo d: foo
`, `,
expected: ` expected: `
apiVersion: v1 apiVersion: v1
kind: Bar kind: Bar
a: a:
b/c: b/c:
d: bar d: bar
`, `,
filter: fieldspec.Filter{ filter: fieldspec.Filter{
SetValue: filtersutil.SetScalar("bar"), SetValue: filtersutil.SetScalar("bar"),
CreateKind: yaml.ScalarNode, CreateKind: yaml.ScalarNode,
},
}, },
"fieldname with multiple '/'": { },
fieldSpec: ` {
name: "filedname with multiple '/'",
fieldSpec: `
path: a/b\/c/d\/e/f path: a/b\/c/d\/e/f
version: v1 version: v1
kind: Bar kind: Bar
`, `,
input: ` input: `
apiVersion: v1 apiVersion: v1
kind: Bar kind: Bar
a: a:
@@ -501,7 +476,7 @@ a:
d/e: d/e:
f: foo f: foo
`, `,
expected: ` expected: `
apiVersion: v1 apiVersion: v1
kind: Bar kind: Bar
a: a:
@@ -509,24 +484,25 @@ a:
d/e: d/e:
f: bar f: bar
`, `,
filter: fieldspec.Filter{ filter: fieldspec.Filter{
SetValue: filtersutil.SetScalar("bar"), SetValue: filtersutil.SetScalar("bar"),
CreateKind: yaml.ScalarNode, CreateKind: yaml.ScalarNode,
},
}, },
} },
}
for n := range testCases { func TestFilter_Filter(t *testing.T) {
tc := testCases[n] for i := range tests {
t.Run(n, func(t *testing.T) { test := tests[i]
err := yaml.Unmarshal([]byte(tc.fieldSpec), &tc.filter.FieldSpec) t.Run(test.name, func(t *testing.T) {
err := yaml.Unmarshal([]byte(test.fieldSpec), &test.filter.FieldSpec)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.FailNow() t.FailNow()
} }
out := &bytes.Buffer{} out := &bytes.Buffer{}
rw := &kio.ByteReadWriter{ rw := &kio.ByteReadWriter{
Reader: bytes.NewBufferString(tc.input), Reader: bytes.NewBufferString(test.input),
Writer: out, Writer: out,
OmitReaderAnnotations: true, OmitReaderAnnotations: true,
} }
@@ -534,11 +510,11 @@ a:
// run the filter // run the filter
err = kio.Pipeline{ err = kio.Pipeline{
Inputs: []kio.Reader{rw}, Inputs: []kio.Reader{rw},
Filters: []kio.Filter{kio.FilterAll(tc.filter)}, Filters: []kio.Filter{kio.FilterAll(test.filter)},
Outputs: []kio.Writer{rw}, Outputs: []kio.Writer{rw},
}.Execute() }.Execute()
if tc.error != "" { if test.error != "" {
if !assert.EqualError(t, err, tc.error) { if !assert.EqualError(t, err, test.error) {
t.FailNow() t.FailNow()
} }
// stop rest of test // stop rest of test
@@ -551,92 +527,10 @@ a:
// check results // check results
if !assert.Equal(t, if !assert.Equal(t,
strings.TrimSpace(tc.expected), strings.TrimSpace(test.expected),
strings.TrimSpace(out.String())) { strings.TrimSpace(out.String())) {
t.FailNow() t.FailNow()
} }
}) })
} }
} }
func TestFilter_FieldPaths(t *testing.T) {
testCases := map[string]struct {
input string
fieldSpec string
expected []string
}{
"fieldpath containing SequenceNode": {
input: `
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: store
image: redis:6.2.6
- name: server
image: nginx:latest
`,
fieldSpec: `
path: spec/containers[]/image
kind: Pod
`,
expected: []string{
"spec.containers.image",
"spec.containers.image",
},
},
"fieldpath with MappingNode": {
input: `
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: store
image: redis:6.2.6
- name: server
image: nginx:latest
`,
fieldSpec: `
path: metadata/name
kind: Pod
`,
expected: []string{
"metadata.name",
},
},
}
for name, tc := range testCases {
var fieldPaths []string
trackableSetter := filtersutil.TrackableSetter{}
trackableSetter.WithMutationTracker(func(key, value, tag string, node *yaml.RNode) {
fieldPaths = append(fieldPaths, strings.Join(node.FieldPath(), "."))
})
filter := fieldspec.Filter{
SetValue: trackableSetter.SetScalar("foo"),
}
t.Run(name, func(t *testing.T) {
err := yaml.Unmarshal([]byte(tc.fieldSpec), &filter.FieldSpec)
assert.NoError(t, err)
rw := &kio.ByteReadWriter{
Reader: bytes.NewBufferString(tc.input),
Writer: &bytes.Buffer{},
OmitReaderAnnotations: true,
}
// run the filter
err = kio.Pipeline{
Inputs: []kio.Reader{rw},
Filters: []kio.Filter{kio.FilterAll(filter)},
Outputs: []kio.Writer{rw},
}.Execute()
assert.NoError(t, err)
assert.Equal(t, tc.expected, fieldPaths)
})
}
}

View File

@@ -0,0 +1,49 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package fieldspec
import (
"strings"
"sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// Return true for 'v' followed by a 1 or 2, and don't look at rest.
// I.e. 'v1', 'v1beta1', 'v2', would return true.
func looksLikeACoreApiVersion(s string) bool {
if len(s) < 2 {
return false
}
if s[0:1] != "v" {
return false
}
return s[1:2] == "1" || s[1:2] == "2"
}
// parseGV parses apiVersion field into group and version.
func parseGV(apiVersion string) (group, version string) {
// parse the group and version from the apiVersion field
parts := strings.SplitN(apiVersion, "/", 2)
group = parts[0]
if len(parts) > 1 {
version = parts[1]
}
// Special case the original "apiVersion" of what
// we now call the "core" (empty) group.
if version == "" && looksLikeACoreApiVersion(group) {
version = group
group = ""
}
return
}
// GetGVK parses the metadata into a GVK
func GetGVK(meta yaml.ResourceMeta) resid.Gvk {
group, version := parseGV(meta.APIVersion)
return resid.Gvk{
Group: group,
Version: version,
Kind: meta.Kind,
}
}

View File

@@ -0,0 +1,156 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package fieldspec
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
func TestParseGV(t *testing.T) {
testCases := map[string]struct {
input string
expectedGroup string
expectedVersion string
}{
"empty": {
input: "",
expectedGroup: "",
expectedVersion: "",
},
"certSigning": {
input: "certificates.k8s.io/v1beta1",
expectedGroup: "certificates.k8s.io",
expectedVersion: "v1beta1",
},
"extensions": {
input: "extensions/v1beta1",
expectedGroup: "extensions",
expectedVersion: "v1beta1",
},
"normal": {
input: "apps/v1",
expectedGroup: "apps",
expectedVersion: "v1",
},
"justApps": {
input: "apps",
expectedGroup: "apps",
expectedVersion: "",
},
"coreV1": {
input: "v1",
expectedGroup: "",
expectedVersion: "v1",
},
"coreV2": {
input: "v2",
expectedGroup: "",
expectedVersion: "v2",
},
"coreV2Beta1": {
input: "v2beta1",
expectedGroup: "",
expectedVersion: "v2beta1",
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
group, version := parseGV(tc.input)
if !assert.Equal(t, tc.expectedGroup, group) {
t.FailNow()
}
if !assert.Equal(t, tc.expectedVersion, version) {
t.FailNow()
}
})
}
}
func TestGetGVK(t *testing.T) {
testCases := map[string]struct {
input string
expected resid.Gvk
parseError string
metaError string
}{
"empty": {
input: `
`,
parseError: "EOF",
},
"junk": {
input: `
congress: effective
`,
metaError: "missing Resource metadata",
},
"normal": {
input: `
apiVersion: apps/v1
kind: Deployment
`,
expected: resid.Gvk{Group: "apps", Version: "v1", Kind: "Deployment"},
},
"apiVersionOnlyWithSlash": {
input: `
apiVersion: apps/v1
`,
expected: resid.Gvk{Group: "apps", Version: "v1", Kind: ""},
},
"apiVersionOnlyNoSlash1": {
input: `
apiVersion: apps
`,
expected: resid.Gvk{Group: "apps", Version: "", Kind: ""},
},
"apiVersionOnlyNoSlash2": {
input: `
apiVersion: v1
`,
expected: resid.Gvk{Group: "", Version: "v1", Kind: ""},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
obj, err := yaml.Parse(tc.input)
if len(tc.parseError) != 0 {
if err == nil {
t.Error("expected parse error")
return
}
if !strings.Contains(err.Error(), tc.parseError) {
t.Errorf("expected parse err '%s', got '%v'", tc.parseError, err)
}
return
}
if !assert.NoError(t, err) {
t.FailNow()
}
meta, err := obj.GetMeta()
if len(tc.metaError) != 0 {
if err == nil {
t.Error("expected meta error")
return
}
if !strings.Contains(err.Error(), tc.metaError) {
t.Errorf("expected meta err '%s', got '%v'", tc.metaError, err)
}
return
}
if !assert.NoError(t, err) {
t.FailNow()
}
gvk := GetGVK(meta)
if !assert.Equal(t, tc.expected, gvk) {
t.FailNow()
}
})
}
}

View File

@@ -0,0 +1,21 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package filtersutil
import (
"sort"
)
// SortedMapKeys returns a sorted slice of keys to the given map.
// Writing this function never gets old.
func SortedMapKeys(m map[string]string) []string {
keys := make([]string, len(m))
i := 0
for k := range m {
keys[i] = k
i++
}
sort.Strings(keys)
return keys
}

View File

@@ -1,13 +1,10 @@
// Copyright 2021 The Kubernetes Authors. package filtersutil_test
// SPDX-License-Identifier: Apache-2.0
package yaml_test
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/api/filters/filtersutil"
) )
func TestSortedKeys(t *testing.T) { func TestSortedKeys(t *testing.T) {
@@ -26,10 +23,9 @@ func TestSortedKeys(t *testing.T) {
expected: []string{"a", "b", "c"}}, expected: []string{"a", "b", "c"}},
} }
for tn, tc := range testCases { for tn, tc := range testCases {
tc := tc
t.Run(tn, func(t *testing.T) { t.Run(tn, func(t *testing.T) {
if !assert.Equal(t, if !assert.Equal(t,
yaml.SortedMapKeys(tc.input), filtersutil.SortedMapKeys(tc.input),
tc.expected) { tc.expected) {
t.FailNow() t.FailNow()
} }

View File

@@ -31,39 +31,3 @@ func SetEntry(key, value, tag string) SetFn {
}) })
} }
} }
type TrackableSetter struct {
// SetValueCallback will be invoked each time a field is set
setValueCallback func(key, value, tag string, node *yaml.RNode)
}
// WithMutationTracker registers a callback which will be invoked each time a field is mutated
func (s *TrackableSetter) WithMutationTracker(callback func(key, value, tag string, node *yaml.RNode)) {
s.setValueCallback = callback
}
// SetScalar returns a SetFn to set a scalar value
// if a mutation tracker has been registered, the tracker will be invoked each
// time a scalar is set
func (s TrackableSetter) SetScalar(value string) SetFn {
origSetScalar := SetScalar(value)
return func(node *yaml.RNode) error {
if s.setValueCallback != nil {
s.setValueCallback("", value, "", node)
}
return origSetScalar(node)
}
}
// SetEntry returns a SetFn to set an entry in a map
// if a mutation tracker has been registered, the tracker will be invoked each
// time an entry is set
func (s TrackableSetter) SetEntry(key, value, tag string) SetFn {
origSetEntry := SetEntry(key, value, tag)
return func(node *yaml.RNode) error {
if s.setValueCallback != nil {
s.setValueCallback(key, value, tag, node)
}
return origSetEntry(node)
}
}

View File

@@ -1,3 +0,0 @@
// Package gkesagenerator contains a kio.Filter that that generates a
// iampolicy-related resources for a given cloud provider
package iampolicygenerator

View File

@@ -1,46 +0,0 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package iampolicygenerator
import (
"log"
"os"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
func ExampleFilter() {
f := Filter{}
var err = yaml.Unmarshal([]byte(`
cloud: gke
kubernetesService:
namespace: k8s-namespace
name: k8s-sa-name
serviceAccount:
name: gsa-name
projectId: project-id
`), &f)
if err != nil {
log.Fatal(err)
}
err = kio.Pipeline{
Inputs: []kio.Reader{},
Filters: []kio.Filter{f},
Outputs: []kio.Writer{kio.ByteWriter{Writer: os.Stdout}},
}.Execute()
if err != nil {
log.Fatal(err)
}
// Output:
// apiVersion: v1
// kind: ServiceAccount
// metadata:
// annotations:
// iam.gke.io/gcp-service-account: gsa-name@project-id.iam.gserviceaccount.com
// name: k8s-sa-name
// namespace: k8s-namespace
}

View File

@@ -1,55 +0,0 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package iampolicygenerator
import (
"fmt"
"sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
type Filter struct {
IAMPolicyGenerator types.IAMPolicyGeneratorArgs `json:",inline,omitempty" yaml:",inline,omitempty"`
}
// Filter adds a GKE service account object to nodes
func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
switch f.IAMPolicyGenerator.Cloud {
case types.GKE:
IAMPolicyResources, err := f.generateGkeIAMPolicyResources()
if err != nil {
return nil, err
}
nodes = append(nodes, IAMPolicyResources...)
default:
return nil, fmt.Errorf("cloud provider %s not supported yet", f.IAMPolicyGenerator.Cloud)
}
return nodes, nil
}
func (f Filter) generateGkeIAMPolicyResources() ([]*yaml.RNode, error) {
var result []*yaml.RNode
input := fmt.Sprintf(`
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
iam.gke.io/gcp-service-account: %s@%s.iam.gserviceaccount.com
name: %s
`, f.IAMPolicyGenerator.ServiceAccount.Name,
f.IAMPolicyGenerator.ProjectId,
f.IAMPolicyGenerator.KubernetesService.Name)
if f.IAMPolicyGenerator.Namespace != "" {
input = input + fmt.Sprintf("\n namespace: %s", f.IAMPolicyGenerator.Namespace)
}
sa, err := yaml.Parse(input)
if err != nil {
return nil, err
}
return append(result, sa), nil
}

View File

@@ -1,75 +0,0 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package iampolicygenerator
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
filtertest "sigs.k8s.io/kustomize/api/testutils/filtertest"
"sigs.k8s.io/kustomize/api/types"
)
func TestFilter(t *testing.T) {
testCases := map[string]struct {
args types.IAMPolicyGeneratorArgs
expected string
}{
"with namespace": {
args: types.IAMPolicyGeneratorArgs{
Cloud: types.GKE,
KubernetesService: types.KubernetesService{
Namespace: "k8s-namespace",
Name: "k8s-sa-name",
},
ServiceAccount: types.ServiceAccount{
Name: "gsa-name",
ProjectId: "project-id",
},
},
expected: `
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
iam.gke.io/gcp-service-account: gsa-name@project-id.iam.gserviceaccount.com
name: k8s-sa-name
namespace: k8s-namespace
`,
},
"without namespace": {
args: types.IAMPolicyGeneratorArgs{
Cloud: types.GKE,
KubernetesService: types.KubernetesService{
Name: "k8s-sa-name",
},
ServiceAccount: types.ServiceAccount{
Name: "gsa-name",
ProjectId: "project-id",
},
},
expected: `
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
iam.gke.io/gcp-service-account: gsa-name@project-id.iam.gserviceaccount.com
name: k8s-sa-name
`,
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
f := Filter{
IAMPolicyGenerator: tc.args,
}
actual := filtertest.RunFilter(t, "", f)
if !assert.Equal(t, strings.TrimSpace(tc.expected), strings.TrimSpace(actual)) {
t.FailNow()
}
})
}
}

View File

@@ -23,17 +23,9 @@ type Filter struct {
// FsSlice contains the FieldSpecs to locate an image field, // FsSlice contains the FieldSpecs to locate an image field,
// e.g. Path: "spec/myContainers[]/image" // e.g. Path: "spec/myContainers[]/image"
FsSlice types.FsSlice `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"` FsSlice types.FsSlice `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"`
trackableSetter filtersutil.TrackableSetter
} }
var _ kio.Filter = Filter{} var _ kio.Filter = Filter{}
var _ kio.TrackableFilter = &Filter{}
// WithMutationTracker registers a callback which will be invoked each time a field is mutated
func (f *Filter) WithMutationTracker(callback func(key, value, tag string, node *yaml.RNode)) {
f.trackableSetter.WithMutationTracker(callback)
}
func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
_, err := kio.FilterAll(yaml.FilterFunc(f.filter)).Filter(nodes) _, err := kio.FilterAll(yaml.FilterFunc(f.filter)).Filter(nodes)
@@ -48,11 +40,8 @@ func (f Filter) filter(node *yaml.RNode) (*yaml.RNode, error) {
return node, nil return node, nil
} }
if err := node.PipeE(fsslice.Filter{ if err := node.PipeE(fsslice.Filter{
FsSlice: f.FsSlice, FsSlice: f.FsSlice,
SetValue: imageTagUpdater{ SetValue: updateImageTagFn(f.ImageTag),
ImageTag: f.ImageTag,
trackableSetter: f.trackableSetter,
}.SetImageValue,
}); err != nil { }); err != nil {
return nil, err return nil, err
} }
@@ -70,3 +59,11 @@ func (f Filter) isOnDenyList(node *yaml.RNode) bool {
// https://github.com/kubernetes-sigs/kustomize/issues/890 // https://github.com/kubernetes-sigs/kustomize/issues/890
return meta.Kind == `CustomResourceDefinition` return meta.Kind == `CustomResourceDefinition`
} }
func updateImageTagFn(imageTag types.Image) filtersutil.SetFn {
return func(node *yaml.RNode) error {
return node.PipeE(imageTagUpdater{
ImageTag: imageTag,
})
}
}

View File

@@ -10,22 +10,18 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
filtertest "sigs.k8s.io/kustomize/api/testutils/filtertest" filtertest "sigs.k8s.io/kustomize/api/testutils/filtertest"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/yaml"
) )
func TestImageTagUpdater_Filter(t *testing.T) { func TestImageTagUpdater_Filter(t *testing.T) {
mutationTrackerStub := filtertest.MutationTrackerStub{}
testCases := map[string]struct { testCases := map[string]struct {
input string input string
expectedOutput string expectedOutput string
filter Filter filter Filter
fsSlice types.FsSlice fsSlice types.FsSlice
setValueCallback func(key, value, tag string, node *yaml.RNode)
expectedSetValueArgs []filtertest.SetValueArg
}{ }{
"ignore CustomResourceDefinition": { "ignore CustomResourceDefinition": {
input: ` input: `
apiVersion: apiextensions.k8s.io/v1 apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
name: whatever name: whatever
@@ -34,7 +30,7 @@ spec:
- image: whatever - image: whatever
`, `,
expectedOutput: ` expectedOutput: `
apiVersion: apiextensions.k8s.io/v1 apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
name: whatever name: whatever
@@ -662,108 +658,17 @@ spec:
}, },
}, },
}, },
"mutation tracker": {
input: `
group: apps
apiVersion: v1
kind: Deployment
metadata:
name: deploy1
spec:
template:
spec:
containers:
- image: nginx:1.7.9
name: nginx-tagged
- image: nginx:latest
name: nginx-latest
- image: foobar:1
name: replaced-with-digest
- image: postgres:1.8.0
name: postgresdb
initContainers:
- image: nginx
name: nginx-notag
- image: nginx@sha256:111111111111111111
name: nginx-sha256
- image: alpine:1.8.0
name: init-alpine
`,
expectedOutput: `
group: apps
apiVersion: v1
kind: Deployment
metadata:
name: deploy1
spec:
template:
spec:
containers:
- image: busybox:v3
name: nginx-tagged
- image: busybox:v3
name: nginx-latest
- image: foobar:1
name: replaced-with-digest
- image: postgres:1.8.0
name: postgresdb
initContainers:
- image: busybox:v3
name: nginx-notag
- image: busybox:v3
name: nginx-sha256
- image: alpine:1.8.0
name: init-alpine
`,
filter: Filter{
ImageTag: types.Image{
Name: "nginx",
NewName: "busybox",
NewTag: "v3",
},
},
fsSlice: []types.FieldSpec{
{
Path: "spec/template/spec/containers[]/image",
},
{
Path: "spec/template/spec/initContainers[]/image",
},
},
setValueCallback: mutationTrackerStub.MutationTracker,
expectedSetValueArgs: []filtertest.SetValueArg{
{
Value: "busybox:v3",
NodePath: []string{"spec", "template", "spec", "containers", "image"},
},
{
Value: "busybox:v3",
NodePath: []string{"spec", "template", "spec", "containers", "image"},
},
{
Value: "busybox:v3",
NodePath: []string{"spec", "template", "spec", "initContainers", "image"},
},
{
Value: "busybox:v3",
NodePath: []string{"spec", "template", "spec", "initContainers", "image"},
},
},
},
} }
for tn, tc := range testCases { for tn, tc := range testCases {
mutationTrackerStub.Reset()
t.Run(tn, func(t *testing.T) { t.Run(tn, func(t *testing.T) {
filter := tc.filter filter := tc.filter
filter.WithMutationTracker(tc.setValueCallback)
filter.FsSlice = tc.fsSlice filter.FsSlice = tc.fsSlice
if !assert.Equal(t, if !assert.Equal(t,
strings.TrimSpace(tc.expectedOutput), strings.TrimSpace(tc.expectedOutput),
strings.TrimSpace(filtertest.RunFilter(t, tc.input, filter))) { strings.TrimSpace(filtertest.RunFilter(t, tc.input, filter))) {
t.FailNow() t.FailNow()
} }
assert.Equal(t, tc.expectedSetValueArgs, mutationTrackerStub.SetValueArgs())
}) })
} }
} }

View File

@@ -4,7 +4,6 @@
package imagetag package imagetag
import ( import (
"sigs.k8s.io/kustomize/api/internal/utils"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml"
@@ -75,7 +74,7 @@ func (f findFieldsFilter) walk(node *yaml.RNode) error {
return err return err
} }
key := n.Key.YNode().Value key := n.Key.YNode().Value
if utils.StringSliceContains(f.fields, key) { if contains(f.fields, key) {
return f.fieldCallback(n.Value) return f.fieldCallback(n.Value)
} }
return nil return nil
@@ -88,6 +87,15 @@ func (f findFieldsFilter) walk(node *yaml.RNode) error {
return nil return nil
} }
func contains(slice []string, str string) bool {
for _, s := range slice {
if s == str {
return true
}
}
return false
}
func checkImageTagsFn(imageTag types.Image) fieldCallback { func checkImageTagsFn(imageTag types.Image) fieldCallback {
return func(node *yaml.RNode) error { return func(node *yaml.RNode) error {
if node.YNode().Kind != yaml.SequenceNode { if node.YNode().Kind != yaml.SequenceNode {

View File

@@ -4,7 +4,6 @@
package imagetag package imagetag
import ( import (
"sigs.k8s.io/kustomize/api/filters/filtersutil"
"sigs.k8s.io/kustomize/api/image" "sigs.k8s.io/kustomize/api/image"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml"
@@ -14,20 +13,19 @@ import (
// that will update the value of the yaml node based on the provided // that will update the value of the yaml node based on the provided
// ImageTag if the current value matches the format of an image reference. // ImageTag if the current value matches the format of an image reference.
type imageTagUpdater struct { type imageTagUpdater struct {
Kind string `yaml:"kind,omitempty"` Kind string `yaml:"kind,omitempty"`
ImageTag types.Image `yaml:"imageTag,omitempty"` ImageTag types.Image `yaml:"imageTag,omitempty"`
trackableSetter filtersutil.TrackableSetter
} }
func (u imageTagUpdater) SetImageValue(rn *yaml.RNode) error { func (u imageTagUpdater) Filter(rn *yaml.RNode) (*yaml.RNode, error) {
if err := yaml.ErrorIfInvalid(rn, yaml.ScalarNode); err != nil { if err := yaml.ErrorIfInvalid(rn, yaml.ScalarNode); err != nil {
return err return nil, err
} }
value := rn.YNode().Value value := rn.YNode().Value
if !image.IsImageMatched(value, u.ImageTag.Name) { if !image.IsImageMatched(value, u.ImageTag.Name) {
return nil return rn, nil
} }
name, tag := image.Split(value) name, tag := image.Split(value)
@@ -41,12 +39,5 @@ func (u imageTagUpdater) SetImageValue(rn *yaml.RNode) error {
tag = "@" + u.ImageTag.Digest tag = "@" + u.ImageTag.Digest
} }
return u.trackableSetter.SetScalar(name + tag)(rn) return rn.Pipe(yaml.FieldSetter{StringValue: name + tag})
}
func (u imageTagUpdater) Filter(rn *yaml.RNode) (*yaml.RNode, error) {
if err := u.SetImageValue(rn); err != nil {
return nil, err
}
return rn, nil
} }

View File

@@ -20,26 +20,18 @@ type Filter struct {
// FsSlice identifies the label fields. // FsSlice identifies the label fields.
FsSlice types.FsSlice FsSlice types.FsSlice
trackableSetter filtersutil.TrackableSetter
} }
var _ kio.Filter = Filter{} var _ kio.Filter = Filter{}
var _ kio.TrackableFilter = &Filter{}
// WithMutationTracker registers a callback which will be invoked each time a field is mutated
func (f *Filter) WithMutationTracker(callback func(key, value, tag string, node *yaml.RNode)) {
f.trackableSetter.WithMutationTracker(callback)
}
func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
keys := yaml.SortedMapKeys(f.Labels) keys := filtersutil.SortedMapKeys(f.Labels)
_, err := kio.FilterAll(yaml.FilterFunc( _, err := kio.FilterAll(yaml.FilterFunc(
func(node *yaml.RNode) (*yaml.RNode, error) { func(node *yaml.RNode) (*yaml.RNode, error) {
for _, k := range keys { for _, k := range keys {
if err := node.PipeE(fsslice.Filter{ if err := node.PipeE(fsslice.Filter{
FsSlice: f.FsSlice, FsSlice: f.FsSlice,
SetValue: f.trackableSetter.SetEntry( SetValue: filtersutil.SetEntry(
k, f.Labels[k], yaml.NodeTagString), k, f.Labels[k], yaml.NodeTagString),
CreateKind: yaml.MappingNode, // Labels are MappingNodes. CreateKind: yaml.MappingNode, // Labels are MappingNodes.
CreateTag: yaml.NodeTagMap, CreateTag: yaml.NodeTagMap,

View File

@@ -8,20 +8,16 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/api/resid"
filtertest_test "sigs.k8s.io/kustomize/api/testutils/filtertest" filtertest_test "sigs.k8s.io/kustomize/api/testutils/filtertest"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/resid"
"sigs.k8s.io/kustomize/kyaml/yaml"
) )
func TestLabels_Filter(t *testing.T) { func TestLabels_Filter(t *testing.T) {
mutationTrackerStub := filtertest_test.MutationTrackerStub{}
testCases := map[string]struct { testCases := map[string]struct {
input string input string
expectedOutput string expectedOutput string
filter Filter filter Filter
setEntryCallback func(key, value, tag string, node *yaml.RNode)
expectedSetEntryArgs []filtertest_test.SetValueArg
}{ }{
"add": { "add": {
input: ` input: `
@@ -403,74 +399,15 @@ metadata:
}, },
}, },
}, },
// test usage of SetEntryCallback
"set_entry_callback": {
input: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: instance
labels:
witcher: geralt
`,
expectedOutput: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: instance
labels:
witcher: geralt
mage: yennefer
a:
b:
mage: yennefer
`,
filter: Filter{
Labels: labelMap{
"mage": "yennefer",
},
FsSlice: []types.FieldSpec{
{
Path: "metadata/labels",
CreateIfNotPresent: true,
},
{
Path: "a/b",
CreateIfNotPresent: true,
},
},
},
setEntryCallback: mutationTrackerStub.MutationTracker,
expectedSetEntryArgs: []filtertest_test.SetValueArg{
{
Key: "mage",
Value: "yennefer",
Tag: "!!str",
NodePath: []string{"metadata", "labels"},
},
{
Key: "mage",
Value: "yennefer",
Tag: "!!str",
NodePath: []string{"a", "b"},
},
},
},
} }
for tn, tc := range testCases { for tn, tc := range testCases {
mutationTrackerStub.Reset()
t.Run(tn, func(t *testing.T) { t.Run(tn, func(t *testing.T) {
tc.filter.WithMutationTracker(tc.setEntryCallback)
if !assert.Equal(t, if !assert.Equal(t,
strings.TrimSpace(tc.expectedOutput), strings.TrimSpace(tc.expectedOutput),
strings.TrimSpace(filtertest_test.RunFilter(t, tc.input, tc.filter))) { strings.TrimSpace(filtertest_test.RunFilter(t, tc.input, tc.filter))) {
t.FailNow() t.FailNow()
} }
if !assert.Equal(t, tc.expectedSetEntryArgs, mutationTrackerStub.SetValueArgs()) {
t.FailNow()
}
}) })
} }
} }

View File

@@ -2,74 +2,37 @@ package nameref
import ( import (
"fmt" "fmt"
"strings"
"github.com/pkg/errors"
"sigs.k8s.io/kustomize/api/filters/fieldspec" "sigs.k8s.io/kustomize/api/filters/fieldspec"
"sigs.k8s.io/kustomize/api/filters/filtersutil"
"sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/resource" "sigs.k8s.io/kustomize/api/resource"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/resid"
"sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml"
) )
// Filter updates a name references. // Filter will update the name reference
type Filter struct { type Filter struct {
// Referrer refers to another resource X by X's name. FieldSpec types.FieldSpec `json:"fieldSpec,omitempty" yaml:"fieldSpec,omitempty"`
// E.g. A Deployment can refer to a ConfigMap. Referrer *resource.Resource
// The Deployment is the Referrer, Target resid.Gvk
// the ConfigMap is the ReferralTarget.
// This filter seeks to repair the reference in Deployment, given
// that the ConfigMap's name may have changed.
Referrer *resource.Resource
// NameFieldToUpdate is the field in the Referrer
// that holds the name requiring an update.
// This is the field to write.
NameFieldToUpdate types.FieldSpec
// ReferralTarget is the source of the new value for
// the name, always in the 'metadata/name' field.
// This is the field to read.
ReferralTarget resid.Gvk
// Set of resources to scan to find the ReferralTarget.
ReferralCandidates resmap.ResMap ReferralCandidates resmap.ResMap
} }
// At time of writing, in practice this is called with a slice with only
// one entry, the node also referred to be the resource in the Referrer field.
func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
return kio.FilterAll(yaml.FilterFunc(f.run)).Filter(nodes) return kio.FilterAll(yaml.FilterFunc(f.run)).Filter(nodes)
} }
// The node passed in here is the same node as held in Referrer;
// that's how the referrer's name field is updated.
// Currently, however, this filter still needs the extra methods on Referrer
// to consult things like the resource Id, its namespace, etc.
// TODO(3455): No filter should use the Resource api; all information
// about names should come from annotations, with helper methods
// on the RNode object. Resource should get stupider, RNode smarter.
func (f Filter) run(node *yaml.RNode) (*yaml.RNode, error) { func (f Filter) run(node *yaml.RNode) (*yaml.RNode, error) {
if err := f.confirmNodeMatchesReferrer(node); err != nil { err := node.PipeE(fieldspec.Filter{
// sanity check. FieldSpec: f.FieldSpec,
return nil, err
}
f.NameFieldToUpdate.Gvk = f.Referrer.GetGvk()
if err := node.PipeE(fieldspec.Filter{
FieldSpec: f.NameFieldToUpdate,
SetValue: f.set, SetValue: f.set,
}); err != nil { })
return nil, errors.Wrapf( return node, err
err, "updating name reference in '%s' field of '%s'",
f.NameFieldToUpdate.Path, f.Referrer.CurId().String())
}
return node, nil
} }
// This function is called on the node found at FieldSpec.Path.
// It's some node in the Referrer.
func (f Filter) set(node *yaml.RNode) error { func (f Filter) set(node *yaml.RNode) error {
if yaml.IsMissingOrNull(node) { if yaml.IsMissingOrNull(node) {
return nil return nil
@@ -78,319 +41,193 @@ func (f Filter) set(node *yaml.RNode) error {
case yaml.ScalarNode: case yaml.ScalarNode:
return f.setScalar(node) return f.setScalar(node)
case yaml.MappingNode: case yaml.MappingNode:
// Kind: ValidatingWebhookConfiguration
// FieldSpec is webhooks/clientConfig/service
return f.setMapping(node) return f.setMapping(node)
case yaml.SequenceNode: case yaml.SequenceNode:
return applyFilterToSeq(seqFilter{ return f.setSequence(node)
setScalarFn: f.setScalar,
setMappingFn: f.setMapping,
}, node)
default: default:
return fmt.Errorf("node must be a scalar, sequence or map") return fmt.Errorf(
"node is expected to be either a string or a slice of string or a map of string")
} }
} }
// This method used when NameFieldToUpdate doesn't lead to func (f Filter) setSequence(node *yaml.RNode) error {
// one scalar field (typically called 'name'), but rather return applyFilterToSeq(seqFilter{
// leads to a map field (called anything). In this case we setScalarFn: f.setScalar,
// must complete the field path, looking for both a 'name' setMappingFn: f.setMapping,
// and a 'namespace' field to help select the proper }, node)
// ReferralTarget to read the name and namespace from. }
func (f Filter) setMapping(node *yaml.RNode) error { func (f Filter) setMapping(node *yaml.RNode) error {
if node.YNode().Kind != yaml.MappingNode { return setNameAndNs(
return fmt.Errorf("expect a mapping node") node,
} f.Referrer,
nameNode, err := node.Pipe(yaml.FieldMatcher{Name: "name"}) f.Target,
if err != nil { f.ReferralCandidates,
return errors.Wrap(err, "trying to match 'name' field") )
}
if nameNode == nil {
// This is a _configuration_ error; the field path
// specified in NameFieldToUpdate.Path doesn't resolve
// to a map with a 'name' field, so we have no idea what
// field to update with a new name.
return fmt.Errorf("path config error; no 'name' field in node")
}
candidates, err := f.filterMapCandidatesByNamespace(node)
if err != nil {
return err
}
oldName := nameNode.YNode().Value
referral, err := f.selectReferral(oldName, candidates)
if err != nil || referral == nil {
// Nil referral means nothing to do.
return err
}
f.recordTheReferral(referral)
if referral.GetName() == oldName && referral.GetNamespace() == "" {
// The name has not changed, nothing to do.
return nil
}
if err = node.PipeE(yaml.FieldSetter{
Name: "name",
StringValue: referral.GetName(),
}); err != nil {
return err
}
if referral.GetNamespace() == "" {
// Don't write an empty string into the namespace field, as
// it should not replace the value "default". The empty
// string is handled as a wild card here, not as an implicit
// specification of the "default" k8s namespace.
return nil
}
return node.PipeE(yaml.FieldSetter{
Name: "namespace",
StringValue: referral.GetNamespace(),
})
}
func (f Filter) filterMapCandidatesByNamespace(
node *yaml.RNode) ([]*resource.Resource, error) {
namespaceNode, err := node.Pipe(yaml.FieldMatcher{Name: "namespace"})
if err != nil {
return nil, errors.Wrap(err, "trying to match 'namespace' field")
}
if namespaceNode == nil {
return f.ReferralCandidates.Resources(), nil
}
namespace := namespaceNode.YNode().Value
nsMap := f.ReferralCandidates.GroupedByOriginalNamespace()
if candidates, ok := nsMap[namespace]; ok {
return candidates, nil
}
nsMap = f.ReferralCandidates.GroupedByCurrentNamespace()
// This could be nil, or an empty list.
return nsMap[namespace], nil
} }
func (f Filter) setScalar(node *yaml.RNode) error { func (f Filter) setScalar(node *yaml.RNode) error {
referral, err := f.selectReferral( newValue, err := getSimpleNameField(
node.YNode().Value, f.ReferralCandidates.Resources()) node.YNode().Value,
if err != nil || referral == nil { f.Referrer,
// Nil referral means nothing to do. f.Target,
f.ReferralCandidates,
f.ReferralCandidates.Resources(),
)
if err != nil {
return err return err
} }
f.recordTheReferral(referral) err = filtersutil.SetScalar(newValue)(node)
if referral.GetName() == node.YNode().Value {
// The name has not changed, nothing to do.
return nil
}
return node.PipeE(yaml.FieldSetter{StringValue: referral.GetName()})
}
// In the resource, make a note that it is referred to by the Referrer.
func (f Filter) recordTheReferral(referral *resource.Resource) {
referral.AppendRefBy(f.Referrer.CurId())
}
// getRoleRefGvk returns a Gvk in the roleRef field. Return error
// if the roleRef, roleRef/apiGroup or roleRef/kind is missing.
func getRoleRefGvk(n *resource.Resource) (*resid.Gvk, error) {
roleRef, err := n.Pipe(yaml.Lookup("roleRef"))
if err != nil { if err != nil {
return nil, err return err
} }
if roleRef.IsNil() { return nil
return nil, fmt.Errorf("roleRef cannot be found in %s", n.MustString())
}
apiGroup, err := roleRef.Pipe(yaml.Lookup("apiGroup"))
if err != nil {
return nil, err
}
if apiGroup.IsNil() {
return nil, fmt.Errorf(
"apiGroup cannot be found in roleRef %s", roleRef.MustString())
}
kind, err := roleRef.Pipe(yaml.Lookup("kind"))
if err != nil {
return nil, err
}
if kind.IsNil() {
return nil, fmt.Errorf(
"kind cannot be found in roleRef %s", roleRef.MustString())
}
return &resid.Gvk{
Group: apiGroup.YNode().Value,
Kind: kind.YNode().Value,
}, nil
} }
// sieveFunc returns true if the resource argument satisfies some criteria. func filterReferralCandidates(
type sieveFunc func(*resource.Resource) bool referrer *resource.Resource,
matches []*resource.Resource) []*resource.Resource {
// doSieve uses a function to accept or ignore resources from a list. var ret []*resource.Resource
// If list is nil, returns immediately. for _, m := range matches {
// It's a filter obviously, but that term is overloaded here. if referrer.PrefixesSuffixesEquals(m) {
func doSieve(list []*resource.Resource, fn sieveFunc) (s []*resource.Resource) { ret = append(ret, m)
for _, r := range list {
if fn(r) {
s = append(s, r)
} }
} }
return return ret
} }
func acceptAll(r *resource.Resource) bool { // selectReferral picks the referral among a subset of candidates.
return true // It returns the current name and namespace of the selected candidate.
} // Note that the content of the referricalCandidateSubset slice is most of the time
// identical to the referralCandidates resmap. Still in some cases, such
func previousNameMatches(name string) sieveFunc { // as ClusterRoleBinding, the subset only contains the resources of a specific
return func(r *resource.Resource) bool { // namespace.
for _, id := range r.PrevIds() { func selectReferral(
if id.Name == name {
return true
}
}
return false
}
}
func previousIdSelectedByGvk(gvk *resid.Gvk) sieveFunc {
return func(r *resource.Resource) bool {
for _, id := range r.PrevIds() {
if id.IsSelected(gvk) {
return true
}
}
return false
}
}
// If the we are updating a 'roleRef/name' field, the 'apiGroup' and 'kind'
// fields in the same 'roleRef' map must be considered.
// If either object is cluster-scoped, there can be a referral.
// E.g. a RoleBinding (which exists in a namespace) can refer
// to a ClusterRole (cluster-scoped) object.
// https://kubernetes.io/docs/reference/access-authn-authz/rbac/#role-and-clusterrole
// Likewise, a ClusterRole can refer to a Secret (in a namespace).
// Objects in different namespaces generally cannot refer to other
// with some exceptions (e.g. RoleBinding and ServiceAccount are both
// namespaceable, but the former can refer to accounts in other namespaces).
func (f Filter) roleRefFilter() sieveFunc {
if !strings.HasSuffix(f.NameFieldToUpdate.Path, "roleRef/name") {
return acceptAll
}
roleRefGvk, err := getRoleRefGvk(f.Referrer)
if err != nil {
return acceptAll
}
return previousIdSelectedByGvk(roleRefGvk)
}
func prefixSuffixEquals(other resource.ResCtx) sieveFunc {
return func(r *resource.Resource) bool {
return r.PrefixesSuffixesEquals(other)
}
}
func (f Filter) sameCurrentNamespaceAsReferrer() sieveFunc {
referrerCurId := f.Referrer.CurId()
if referrerCurId.IsClusterScoped() {
// If the referrer is cluster-scoped, let anything through.
return acceptAll
}
return func(r *resource.Resource) bool {
if r.CurId().IsClusterScoped() {
// Allow cluster-scoped through.
return true
}
if r.GetKind() == "ServiceAccount" {
// Allow service accounts through, even though they
// are in a namespace. A RoleBinding in another namespace
// can reference them.
return true
}
return referrerCurId.IsNsEquals(r.CurId())
}
}
// selectReferral picks the best referral from a list of candidates.
func (f Filter) selectReferral(
// The name referral that may need to be updated.
oldName string, oldName string,
candidates []*resource.Resource) (*resource.Resource, error) { referrer *resource.Resource,
candidates = doSieve(candidates, previousNameMatches(oldName)) target resid.Gvk,
candidates = doSieve(candidates, previousIdSelectedByGvk(&f.ReferralTarget)) referralCandidates resmap.ResMap,
candidates = doSieve(candidates, f.roleRefFilter()) referralCandidateSubset []*resource.Resource) (string, string, error) {
candidates = doSieve(candidates, f.sameCurrentNamespaceAsReferrer())
if len(candidates) == 1 {
return candidates[0], nil
}
candidates = doSieve(candidates, prefixSuffixEquals(f.Referrer))
if len(candidates) == 1 {
return candidates[0], nil
}
if len(candidates) == 0 {
return nil, nil
}
if allNamesAreTheSame(candidates) {
// Just take the first one.
return candidates[0], nil
}
ids := getIds(candidates)
f.failureDetails(candidates)
return nil, fmt.Errorf(" found multiple possible referrals: %s", ids)
}
func (f Filter) failureDetails(resources []*resource.Resource) { for _, res := range referralCandidateSubset {
fmt.Printf( id := res.OrgId()
"\n**** Too many possible referral targets to referrer:\n%s\n", if id.IsSelected(&target) && res.GetOriginalName() == oldName {
f.Referrer.MustYaml()) matches := referralCandidates.GetMatchingResourcesByOriginalId(id.Equals)
for i, r := range resources { // If there's more than one match,
fmt.Printf( // filter the matches by prefix and suffix
"--- possible referral %d:\n%s", i, r.MustYaml()) if len(matches) > 1 {
fmt.Println("------") filteredMatches := filterReferralCandidates(referrer, matches)
} if len(filteredMatches) > 1 {
} return "", "", fmt.Errorf(
"multiple matches for %s:\n %v",
func allNamesAreTheSame(resources []*resource.Resource) bool { id, getIds(filteredMatches))
name := resources[0].GetName() }
for i := 1; i < len(resources); i++ { // Check is the match the resource we are working on
if name != resources[i].GetName() { if len(filteredMatches) == 0 || res != filteredMatches[0] {
return false continue
}
}
// In the resource, note that it is referenced
// by the referrer.
res.AppendRefBy(referrer.CurId())
// Return transformed name of the object,
// complete with prefixes, hashes, etc.
return res.GetName(), res.GetNamespace(), nil
} }
} }
return true
return oldName, "", nil
} }
func getIds(rs []*resource.Resource) string { // utility function to replace a simple string by the new name
func getSimpleNameField(
oldName string,
referrer *resource.Resource,
target resid.Gvk,
referralCandidates resmap.ResMap,
referralCandidateSubset []*resource.Resource) (string, error) {
newName, _, err := selectReferral(oldName, referrer, target,
referralCandidates, referralCandidateSubset)
return newName, err
}
func getIds(rs []*resource.Resource) []string {
var result []string var result []string
for _, r := range rs { for _, r := range rs {
result = append(result, r.CurId().String()) result = append(result, r.CurId().String()+"\n")
} }
return strings.Join(result, ", ") return result
} }
func checkEqual(k, a, b string) error { // utility function to replace name field within a map RNode
if a != b { // and leverage the namespace field.
return fmt.Errorf( func setNameAndNs(
"node-referrerOriginal '%s' mismatch '%s' != '%s'", in *yaml.RNode,
k, a, b) referrer *resource.Resource,
} target resid.Gvk,
return nil referralCandidates resmap.ResMap) error {
}
func (f Filter) confirmNodeMatchesReferrer(node *yaml.RNode) error { if in.YNode().Kind != yaml.MappingNode {
meta, err := node.GetMeta() return fmt.Errorf("expect a mapping node")
}
// Get name field
nameNode, err := in.Pipe(yaml.FieldMatcher{
Name: "name",
})
if err != nil || nameNode == nil {
return fmt.Errorf("cannot find field 'name' in node")
}
// Get namespace field
namespaceNode, err := in.Pipe(yaml.FieldMatcher{
Name: "namespace",
})
if err != nil {
return fmt.Errorf("error when find field 'namespace'")
}
// check is namespace matched
// name will bot be updated if the namespace doesn't match
subset := referralCandidates.Resources()
if namespaceNode != nil {
namespace := namespaceNode.YNode().Value
bynamespace := referralCandidates.GroupedByOriginalNamespace()
if _, ok := bynamespace[namespace]; !ok {
return nil
}
subset = bynamespace[namespace]
}
oldName := nameNode.YNode().Value
newname, newnamespace, err := selectReferral(oldName, referrer, target,
referralCandidates, subset)
if err != nil { if err != nil {
return err return err
} }
gvk := f.Referrer.GetGvk()
if err = checkEqual( if (newname == oldName) && (newnamespace == "") {
"APIVersion", meta.APIVersion, gvk.ApiVersion()); err != nil { // no candidate found.
return err return nil
} }
if err = checkEqual(
"Kind", meta.Kind, gvk.Kind); err != nil { // set name
return err in.Pipe(yaml.FieldSetter{
} Name: "name",
if err = checkEqual( StringValue: newname,
"Name", meta.Name, f.Referrer.GetName()); err != nil { })
return err if newnamespace != "" {
} // We don't want value "" to replace value "default" since
if err = checkEqual( // the empty string is handled as a wild card here not default namespace
"Namespace", meta.Namespace, f.Referrer.GetNamespace()); err != nil { // by kubernetes.
return err in.Pipe(yaml.FieldSetter{
Name: "namespace",
StringValue: newnamespace,
})
} }
return nil return nil
} }

View File

@@ -5,23 +5,24 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/api/provider" "sigs.k8s.io/kustomize/api/k8sdeps/kunstruct"
"sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/resource"
filtertest_test "sigs.k8s.io/kustomize/api/testutils/filtertest" filtertest_test "sigs.k8s.io/kustomize/api/testutils/filtertest"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/resid"
) )
func TestNamerefFilter(t *testing.T) { func TestNamerefFilter(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
referrerOriginal string input string
candidates string candidates string
referrerFinal string expected string
filter Filter filter Filter
originalNames []string originalNames []string
}{ }{
"simple scalar": { "simple scalar": {
referrerOriginal: ` input: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -40,8 +41,8 @@ kind: NotSecret
metadata: metadata:
name: newName2 name: newName2
`, `,
originalNames: []string{"oldName", "newName2"}, originalNames: []string{"oldName", ""},
referrerFinal: ` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -50,8 +51,8 @@ ref:
name: newName name: newName
`, `,
filter: Filter{ filter: Filter{
NameFieldToUpdate: types.FieldSpec{Path: "ref/name"}, FieldSpec: types.FieldSpec{Path: "ref/name"},
ReferralTarget: resid.Gvk{ Target: resid.Gvk{
Group: "apps", Group: "apps",
Version: "v1", Version: "v1",
Kind: "Secret", Kind: "Secret",
@@ -59,7 +60,7 @@ ref:
}, },
}, },
"sequence": { "sequence": {
referrerOriginal: ` input: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -79,8 +80,8 @@ kind: NotSecret
metadata: metadata:
name: newName2 name: newName2
`, `,
originalNames: []string{"oldName1", "newName2"}, originalNames: []string{"oldName1", ""},
referrerFinal: ` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -90,8 +91,8 @@ seq:
- oldName2 - oldName2
`, `,
filter: Filter{ filter: Filter{
NameFieldToUpdate: types.FieldSpec{Path: "seq"}, FieldSpec: types.FieldSpec{Path: "seq"},
ReferralTarget: resid.Gvk{ Target: resid.Gvk{
Group: "apps", Group: "apps",
Version: "v1", Version: "v1",
Kind: "Secret", Kind: "Secret",
@@ -99,7 +100,7 @@ seq:
}, },
}, },
"mapping": { "mapping": {
referrerOriginal: ` input: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -118,8 +119,8 @@ kind: NotSecret
metadata: metadata:
name: newName2 name: newName2
`, `,
originalNames: []string{"oldName", "newName2"}, originalNames: []string{"oldName", ""},
referrerFinal: ` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -128,8 +129,8 @@ map:
name: newName name: newName
`, `,
filter: Filter{ filter: Filter{
NameFieldToUpdate: types.FieldSpec{Path: "map"}, FieldSpec: types.FieldSpec{Path: "map"},
ReferralTarget: resid.Gvk{ Target: resid.Gvk{
Group: "apps", Group: "apps",
Version: "v1", Version: "v1",
Kind: "Secret", Kind: "Secret",
@@ -137,85 +138,40 @@ map:
}, },
}, },
"mapping with namespace": { "mapping with namespace": {
referrerOriginal: ` input: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: dep name: dep
namespace: someNs
map: map:
name: oldName name: oldName
namespace: someNs namespace: oldNs
`,
candidates: `
apiVersion: apps/v1
kind: Secret
metadata:
name: newName
namespace: someNs
---
apiVersion: apps/v1
kind: NotSecret
metadata:
name: newName2
---
apiVersion: apps/v1
kind: Secret
metadata:
name: thirdName
`,
originalNames: []string{"oldName", "oldName", "oldName"},
referrerFinal: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: dep
namespace: someNs
map:
name: newName
namespace: someNs
`,
filter: Filter{
NameFieldToUpdate: types.FieldSpec{Path: "map"},
ReferralTarget: resid.Gvk{
Group: "apps",
Version: "v1",
Kind: "Secret",
},
},
},
"null value": {
referrerOriginal: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: dep
map:
name: null
`, `,
candidates: ` candidates: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Secret kind: Secret
metadata: metadata:
name: newName name: newName
namespace: oldNs
--- ---
apiVersion: apps/v1 apiVersion: apps/v1
kind: NotSecret kind: NotSecret
metadata: metadata:
name: newName2 name: newName2
`, `,
originalNames: []string{"oldName", "newName2"}, originalNames: []string{"oldName", ""},
referrerFinal: ` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: dep name: dep
map: map:
name: null name: newName
namespace: oldNs
`, `,
filter: Filter{ filter: Filter{
NameFieldToUpdate: types.FieldSpec{Path: "map"}, FieldSpec: types.FieldSpec{Path: "map"},
ReferralTarget: resid.Gvk{ Target: resid.Gvk{
Group: "apps", Group: "apps",
Version: "v1", Version: "v1",
Kind: "Secret", Kind: "Secret",
@@ -226,14 +182,14 @@ map:
for tn, tc := range testCases { for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) { t.Run(tn, func(t *testing.T) {
factory := provider.NewDefaultDepProvider().GetResourceFactory() factory := resource.NewFactory(kunstruct.NewKunstructuredFactoryImpl())
referrer, err := factory.FromBytes([]byte(tc.referrerOriginal)) referrer, err := factory.FromBytes([]byte(tc.input))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
tc.filter.Referrer = referrer tc.filter.Referrer = referrer
resMapFactory := resmap.NewFactory(factory) resMapFactory := resmap.NewFactory(factory, nil)
candidatesRes, err := factory.SliceFromBytesWithNames( candidatesRes, err := factory.SliceFromBytesWithNames(
tc.originalNames, []byte(tc.candidates)) tc.originalNames, []byte(tc.candidates))
if err != nil { if err != nil {
@@ -243,10 +199,10 @@ map:
candidates := resMapFactory.FromResourceSlice(candidatesRes) candidates := resMapFactory.FromResourceSlice(candidatesRes)
tc.filter.ReferralCandidates = candidates tc.filter.ReferralCandidates = candidates
result := filtertest_test.RunFilter(t, tc.referrerOriginal, tc.filter)
if !assert.Equal(t, if !assert.Equal(t,
strings.TrimSpace(tc.referrerFinal), strings.TrimSpace(tc.expected),
strings.TrimSpace(result)) { strings.TrimSpace(
filtertest_test.RunFilter(t, tc.input, tc.filter))) {
t.FailNow() t.FailNow()
} }
}) })
@@ -255,14 +211,35 @@ map:
func TestNamerefFilterUnhappy(t *testing.T) { func TestNamerefFilterUnhappy(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
referrerOriginal string input string
candidates string candidates string
referrerFinal string expected string
filter Filter filter Filter
originalNames []string originalNames []string
}{ }{
"invalid node type": {
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: dep
ref:
name: null
`,
candidates: "",
originalNames: []string{},
expected: "obj '' at path 'ref/name': node is expected to be either a string or a slice of string or a map of string",
filter: Filter{
FieldSpec: types.FieldSpec{Path: "ref/name"},
Target: resid.Gvk{
Group: "apps",
Version: "v1",
Kind: "Secret",
},
},
},
"multiple match": { "multiple match": {
referrerOriginal: ` input: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -282,10 +259,10 @@ metadata:
name: newName2 name: newName2
`, `,
originalNames: []string{"oldName", "oldName"}, originalNames: []string{"oldName", "oldName"},
referrerFinal: "", expected: "",
filter: Filter{ filter: Filter{
NameFieldToUpdate: types.FieldSpec{Path: "ref/name"}, FieldSpec: types.FieldSpec{Path: "ref/name"},
ReferralTarget: resid.Gvk{ Target: resid.Gvk{
Group: "apps", Group: "apps",
Version: "v1", Version: "v1",
Kind: "Secret", Kind: "Secret",
@@ -293,7 +270,7 @@ metadata:
}, },
}, },
"no name": { "no name": {
referrerOriginal: ` input: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -313,10 +290,10 @@ metadata:
name: newName2 name: newName2
`, `,
originalNames: []string{"oldName", "oldName"}, originalNames: []string{"oldName", "oldName"},
referrerFinal: "", expected: "",
filter: Filter{ filter: Filter{
NameFieldToUpdate: types.FieldSpec{Path: "ref"}, FieldSpec: types.FieldSpec{Path: "ref"},
ReferralTarget: resid.Gvk{ Target: resid.Gvk{
Group: "apps", Group: "apps",
Version: "v1", Version: "v1",
Kind: "Secret", Kind: "Secret",
@@ -327,14 +304,14 @@ metadata:
for tn, tc := range testCases { for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) { t.Run(tn, func(t *testing.T) {
factory := provider.NewDefaultDepProvider().GetResourceFactory() factory := resource.NewFactory(kunstruct.NewKunstructuredFactoryImpl())
referrer, err := factory.FromBytes([]byte(tc.referrerOriginal)) referrer, err := factory.FromBytes([]byte(tc.input))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
tc.filter.Referrer = referrer tc.filter.Referrer = referrer
resMapFactory := resmap.NewFactory(factory) resMapFactory := resmap.NewFactory(factory, nil)
candidatesRes, err := factory.SliceFromBytesWithNames( candidatesRes, err := factory.SliceFromBytesWithNames(
tc.originalNames, []byte(tc.candidates)) tc.originalNames, []byte(tc.candidates))
if err != nil { if err != nil {
@@ -344,11 +321,11 @@ metadata:
candidates := resMapFactory.FromResourceSlice(candidatesRes) candidates := resMapFactory.FromResourceSlice(candidatesRes)
tc.filter.ReferralCandidates = candidates tc.filter.ReferralCandidates = candidates
_, err = filtertest_test.RunFilterE(t, tc.referrerOriginal, tc.filter) _, err = filtertest_test.RunFilterE(t, tc.input, tc.filter)
if err == nil { if err == nil {
t.Fatalf("expect an error") t.Fatalf("expect an error")
} }
if tc.referrerFinal != "" && !assert.EqualError(t, err, tc.referrerFinal) { if tc.expected != "" && !assert.EqualError(t, err, tc.expected) {
t.FailNow() t.FailNow()
} }
}) })
@@ -357,19 +334,19 @@ metadata:
func TestCandidatesWithDifferentPrefixSuffix(t *testing.T) { func TestCandidatesWithDifferentPrefixSuffix(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
referrerOriginal string input string
candidates string candidates string
referrerFinal string expected string
filter Filter filter Filter
originalNames []string originalNames []string
prefix []string prefix []string
suffix []string suffix []string
inputPrefix string inputPrefix string
inputSuffix string inputSuffix string
err bool err bool
}{ }{
"prefix match": { "prefix match": {
referrerOriginal: ` input: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -393,7 +370,7 @@ metadata:
suffix: []string{"", "suffix2"}, suffix: []string{"", "suffix2"},
inputPrefix: "prefix1", inputPrefix: "prefix1",
inputSuffix: "", inputSuffix: "",
referrerFinal: ` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -402,8 +379,8 @@ ref:
name: newName name: newName
`, `,
filter: Filter{ filter: Filter{
NameFieldToUpdate: types.FieldSpec{Path: "ref/name"}, FieldSpec: types.FieldSpec{Path: "ref/name"},
ReferralTarget: resid.Gvk{ Target: resid.Gvk{
Group: "apps", Group: "apps",
Version: "v1", Version: "v1",
Kind: "Secret", Kind: "Secret",
@@ -412,7 +389,7 @@ ref:
err: false, err: false,
}, },
"suffix match": { "suffix match": {
referrerOriginal: ` input: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -436,7 +413,7 @@ metadata:
suffix: []string{"suffix1", "suffix2"}, suffix: []string{"suffix1", "suffix2"},
inputPrefix: "", inputPrefix: "",
inputSuffix: "suffix1", inputSuffix: "suffix1",
referrerFinal: ` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -445,8 +422,8 @@ ref:
name: newName name: newName
`, `,
filter: Filter{ filter: Filter{
NameFieldToUpdate: types.FieldSpec{Path: "ref/name"}, FieldSpec: types.FieldSpec{Path: "ref/name"},
ReferralTarget: resid.Gvk{ Target: resid.Gvk{
Group: "apps", Group: "apps",
Version: "v1", Version: "v1",
Kind: "Secret", Kind: "Secret",
@@ -455,7 +432,7 @@ ref:
err: false, err: false,
}, },
"prefix suffix both match": { "prefix suffix both match": {
referrerOriginal: ` input: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -479,7 +456,7 @@ metadata:
suffix: []string{"suffix1", "suffix2"}, suffix: []string{"suffix1", "suffix2"},
inputPrefix: "prefix1", inputPrefix: "prefix1",
inputSuffix: "suffix1", inputSuffix: "suffix1",
referrerFinal: ` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -488,8 +465,8 @@ ref:
name: newName name: newName
`, `,
filter: Filter{ filter: Filter{
NameFieldToUpdate: types.FieldSpec{Path: "ref/name"}, FieldSpec: types.FieldSpec{Path: "ref/name"},
ReferralTarget: resid.Gvk{ Target: resid.Gvk{
Group: "apps", Group: "apps",
Version: "v1", Version: "v1",
Kind: "Secret", Kind: "Secret",
@@ -498,7 +475,7 @@ ref:
err: false, err: false,
}, },
"multiple match: both": { "multiple match: both": {
referrerOriginal: ` input: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -522,10 +499,10 @@ metadata:
suffix: []string{"suffix", "suffix"}, suffix: []string{"suffix", "suffix"},
inputPrefix: "prefix", inputPrefix: "prefix",
inputSuffix: "suffix", inputSuffix: "suffix",
referrerFinal: "", expected: "",
filter: Filter{ filter: Filter{
NameFieldToUpdate: types.FieldSpec{Path: "ref/name"}, FieldSpec: types.FieldSpec{Path: "ref/name"},
ReferralTarget: resid.Gvk{ Target: resid.Gvk{
Group: "apps", Group: "apps",
Version: "v1", Version: "v1",
Kind: "Secret", Kind: "Secret",
@@ -534,7 +511,7 @@ metadata:
err: true, err: true,
}, },
"multiple match: only prefix": { "multiple match: only prefix": {
referrerOriginal: ` input: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -558,10 +535,10 @@ metadata:
suffix: []string{"", ""}, suffix: []string{"", ""},
inputPrefix: "prefix", inputPrefix: "prefix",
inputSuffix: "", inputSuffix: "",
referrerFinal: "", expected: "",
filter: Filter{ filter: Filter{
NameFieldToUpdate: types.FieldSpec{Path: "ref/name"}, FieldSpec: types.FieldSpec{Path: "ref/name"},
ReferralTarget: resid.Gvk{ Target: resid.Gvk{
Group: "apps", Group: "apps",
Version: "v1", Version: "v1",
Kind: "Secret", Kind: "Secret",
@@ -570,7 +547,7 @@ metadata:
err: true, err: true,
}, },
"multiple match: only suffix": { "multiple match: only suffix": {
referrerOriginal: ` input: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -594,10 +571,10 @@ metadata:
suffix: []string{"suffix", "suffix"}, suffix: []string{"suffix", "suffix"},
inputPrefix: "", inputPrefix: "",
inputSuffix: "suffix", inputSuffix: "suffix",
referrerFinal: "", expected: "",
filter: Filter{ filter: Filter{
NameFieldToUpdate: types.FieldSpec{Path: "ref/name"}, FieldSpec: types.FieldSpec{Path: "ref/name"},
ReferralTarget: resid.Gvk{ Target: resid.Gvk{
Group: "apps", Group: "apps",
Version: "v1", Version: "v1",
Kind: "Secret", Kind: "Secret",
@@ -606,7 +583,7 @@ metadata:
err: true, err: true,
}, },
"no match: neither match": { "no match: neither match": {
referrerOriginal: ` input: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -630,7 +607,7 @@ metadata:
suffix: []string{"suffix1", "suffix2"}, suffix: []string{"suffix1", "suffix2"},
inputPrefix: "prefix", inputPrefix: "prefix",
inputSuffix: "suffix", inputSuffix: "suffix",
referrerFinal: ` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -639,8 +616,8 @@ ref:
name: oldName name: oldName
`, `,
filter: Filter{ filter: Filter{
NameFieldToUpdate: types.FieldSpec{Path: "ref/name"}, FieldSpec: types.FieldSpec{Path: "ref/name"},
ReferralTarget: resid.Gvk{ Target: resid.Gvk{
Group: "apps", Group: "apps",
Version: "v1", Version: "v1",
Kind: "Secret", Kind: "Secret",
@@ -649,7 +626,7 @@ ref:
err: false, err: false,
}, },
"no match: prefix doesn't match": { "no match: prefix doesn't match": {
referrerOriginal: ` input: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -673,7 +650,7 @@ metadata:
suffix: []string{"suffix", "suffix"}, suffix: []string{"suffix", "suffix"},
inputPrefix: "prefix", inputPrefix: "prefix",
inputSuffix: "suffix", inputSuffix: "suffix",
referrerFinal: ` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -682,8 +659,8 @@ ref:
name: oldName name: oldName
`, `,
filter: Filter{ filter: Filter{
NameFieldToUpdate: types.FieldSpec{Path: "ref/name"}, FieldSpec: types.FieldSpec{Path: "ref/name"},
ReferralTarget: resid.Gvk{ Target: resid.Gvk{
Group: "apps", Group: "apps",
Version: "v1", Version: "v1",
Kind: "Secret", Kind: "Secret",
@@ -692,7 +669,7 @@ ref:
err: false, err: false,
}, },
"no match: suffix doesn't match": { "no match: suffix doesn't match": {
referrerOriginal: ` input: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -716,7 +693,7 @@ metadata:
suffix: []string{"suffix1", "suffix2"}, suffix: []string{"suffix1", "suffix2"},
inputPrefix: "prefix", inputPrefix: "prefix",
inputSuffix: "suffix", inputSuffix: "suffix",
referrerFinal: ` expected: `
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -725,8 +702,8 @@ ref:
name: oldName name: oldName
`, `,
filter: Filter{ filter: Filter{
NameFieldToUpdate: types.FieldSpec{Path: "ref/name"}, FieldSpec: types.FieldSpec{Path: "ref/name"},
ReferralTarget: resid.Gvk{ Target: resid.Gvk{
Group: "apps", Group: "apps",
Version: "v1", Version: "v1",
Kind: "Secret", Kind: "Secret",
@@ -738,8 +715,8 @@ ref:
for tn, tc := range testCases { for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) { t.Run(tn, func(t *testing.T) {
factory := provider.NewDefaultDepProvider().GetResourceFactory() factory := resource.NewFactory(kunstruct.NewKunstructuredFactoryImpl())
referrer, err := factory.FromBytes([]byte(tc.referrerOriginal)) referrer, err := factory.FromBytes([]byte(tc.input))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -751,7 +728,7 @@ ref:
} }
tc.filter.Referrer = referrer tc.filter.Referrer = referrer
resMapFactory := resmap.NewFactory(factory) resMapFactory := resmap.NewFactory(factory, nil)
candidatesRes, err := factory.SliceFromBytesWithNames( candidatesRes, err := factory.SliceFromBytesWithNames(
tc.originalNames, []byte(tc.candidates)) tc.originalNames, []byte(tc.candidates))
if err != nil { if err != nil {
@@ -771,15 +748,13 @@ ref:
if !tc.err { if !tc.err {
if !assert.Equal(t, if !assert.Equal(t,
strings.TrimSpace(tc.referrerFinal), strings.TrimSpace(tc.expected),
strings.TrimSpace( strings.TrimSpace(
filtertest_test.RunFilter( filtertest_test.RunFilter(t, tc.input, tc.filter))) {
t, tc.referrerOriginal, tc.filter))) {
t.FailNow() t.FailNow()
} }
} else { } else {
_, err := filtertest_test.RunFilterE( _, err := filtertest_test.RunFilterE(t, tc.input, tc.filter)
t, tc.referrerOriginal, tc.filter)
if err == nil { if err == nil {
t.Fatalf("an error is expected") t.Fatalf("an error is expected")
} }

View File

@@ -4,11 +4,11 @@
package namespace package namespace
import ( import (
"sigs.k8s.io/kustomize/api/filters/fieldspec"
"sigs.k8s.io/kustomize/api/filters/filtersutil" "sigs.k8s.io/kustomize/api/filters/filtersutil"
"sigs.k8s.io/kustomize/api/filters/fsslice" "sigs.k8s.io/kustomize/api/filters/fsslice"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/resid"
"sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml"
) )
@@ -18,17 +18,9 @@ type Filter struct {
// FsSlice contains the FieldSpecs to locate the namespace field // FsSlice contains the FieldSpecs to locate the namespace field
FsSlice types.FsSlice `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"` FsSlice types.FsSlice `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"`
trackableSetter filtersutil.TrackableSetter
} }
var _ kio.Filter = Filter{} var _ kio.Filter = Filter{}
var _ kio.TrackableFilter = &Filter{}
// WithMutationTracker registers a callback which will be invoked each time a field is mutated
func (ns *Filter) WithMutationTracker(callback func(key, value, tag string, node *yaml.RNode)) {
ns.trackableSetter.WithMutationTracker(callback)
}
func (ns Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { func (ns Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
return kio.FilterAll(yaml.FilterFunc(ns.run)).Filter(nodes) return kio.FilterAll(yaml.FilterFunc(ns.run)).Filter(nodes)
@@ -52,7 +44,7 @@ func (ns Filter) run(node *yaml.RNode) (*yaml.RNode, error) {
// transformations based on data -- :) // transformations based on data -- :)
err := node.PipeE(fsslice.Filter{ err := node.PipeE(fsslice.Filter{
FsSlice: ns.FsSlice, FsSlice: ns.FsSlice,
SetValue: ns.trackableSetter.SetScalar(ns.Namespace), SetValue: filtersutil.SetScalar(ns.Namespace),
CreateKind: yaml.ScalarNode, // Namespace is a ScalarNode CreateKind: yaml.ScalarNode, // Namespace is a ScalarNode
CreateTag: yaml.NodeTagString, CreateTag: yaml.NodeTagString,
}) })
@@ -62,30 +54,36 @@ func (ns Filter) run(node *yaml.RNode) (*yaml.RNode, error) {
// hacks applies the namespace transforms that are hardcoded rather // hacks applies the namespace transforms that are hardcoded rather
// than specified through FieldSpecs. // than specified through FieldSpecs.
func (ns Filter) hacks(obj *yaml.RNode) error { func (ns Filter) hacks(obj *yaml.RNode) error {
gvk := resid.GvkFromNode(obj) meta, err := obj.GetMeta()
if err := ns.metaNamespaceHack(obj, gvk); err != nil { if err != nil {
return err return err
} }
return ns.roleBindingHack(obj, gvk)
if err := ns.metaNamespaceHack(obj, meta); err != nil {
return err
}
return ns.roleBindingHack(obj, meta)
} }
// metaNamespaceHack is a hack for implementing the namespace transform // metaNamespaceHack is a hack for implementing the namespace transform
// for the metadata.namespace field on namespace scoped resources. // for the metadata.namespace field on namespace scoped resources.
// namespace scoped resources are determined by NOT being present // namespace scoped resources are determined by NOT being present
// in a hard-coded list of cluster-scoped resource types (by apiVersion and kind). // in a blacklist of cluster-scoped resource types (by apiVersion and kind).
// //
// This hack should be updated to allow individual resources to specify // This hack should be updated to allow individual resources to specify
// if they are cluster scoped through either an annotation on the resources, // if they are cluster scoped through either an annotation on the resources,
// or through inlined OpenAPI on the resource as a YAML comment. // or through inlined OpenAPI on the resource as a YAML comment.
func (ns Filter) metaNamespaceHack(obj *yaml.RNode, gvk resid.Gvk) error { func (ns Filter) metaNamespaceHack(obj *yaml.RNode, meta yaml.ResourceMeta) error {
if gvk.IsClusterScoped() { gvk := fieldspec.GetGVK(meta)
if !gvk.IsNamespaceableKind() {
return nil return nil
} }
f := fsslice.Filter{ f := fsslice.Filter{
FsSlice: []types.FieldSpec{ FsSlice: []types.FieldSpec{
{Path: types.MetadataNamespacePath, CreateIfNotPresent: true}, {Path: types.MetadataNamespacePath, CreateIfNotPresent: true},
}, },
SetValue: ns.trackableSetter.SetScalar(ns.Namespace), SetValue: filtersutil.SetScalar(ns.Namespace),
CreateKind: yaml.ScalarNode, // Namespace is a ScalarNode CreateKind: yaml.ScalarNode, // Namespace is a ScalarNode
} }
_, err := f.Filter(obj) _, err := f.Filter(obj)
@@ -106,8 +104,8 @@ func (ns Filter) metaNamespaceHack(obj *yaml.RNode, gvk resid.Gvk) error {
// ... // ...
// - name: "something-else" # this will not have the namespace set // - name: "something-else" # this will not have the namespace set
// ... // ...
func (ns Filter) roleBindingHack(obj *yaml.RNode, gvk resid.Gvk) error { func (ns Filter) roleBindingHack(obj *yaml.RNode, meta yaml.ResourceMeta) error {
if gvk.Kind != roleBindingKind && gvk.Kind != clusterRoleBindingKind { if meta.Kind != roleBindingKind && meta.Kind != clusterRoleBindingKind {
return nil return nil
} }
@@ -120,6 +118,7 @@ func (ns Filter) roleBindingHack(obj *yaml.RNode, gvk resid.Gvk) error {
// add the namespace to each "subject" with name: default // add the namespace to each "subject" with name: default
err = obj.VisitElements(func(o *yaml.RNode) error { err = obj.VisitElements(func(o *yaml.RNode) error {
// copied from kunstruct based kustomize NamespaceTransformer plugin
// The only case we need to force the namespace // The only case we need to force the namespace
// if for the "service account". "default" is // if for the "service account". "default" is
// kind of hardcoded here for right now. // kind of hardcoded here for right now.
@@ -131,15 +130,11 @@ func (ns Filter) roleBindingHack(obj *yaml.RNode, gvk resid.Gvk) error {
} }
// set the namespace for the default account // set the namespace for the default account
node, err := o.Pipe( v := yaml.NewScalarRNode(ns.Namespace)
return o.PipeE(
yaml.LookupCreate(yaml.ScalarNode, "namespace"), yaml.LookupCreate(yaml.ScalarNode, "namespace"),
yaml.FieldSetter{Value: v},
) )
if err != nil {
return err
}
return ns.trackableSetter.SetScalar(ns.Namespace)(node)
}) })
return err return err

View File

@@ -12,11 +12,8 @@ import (
"sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig" "sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig"
filtertest_test "sigs.k8s.io/kustomize/api/testutils/filtertest" filtertest_test "sigs.k8s.io/kustomize/api/testutils/filtertest"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/yaml"
) )
var mutationTrackerStub = filtertest_test.MutationTrackerStub{}
var tests = []TestCase{ var tests = []TestCase{
{ {
name: "add", name: "add",
@@ -197,47 +194,47 @@ metadata:
{ {
name: "update-clusterrolebinding", name: "update-clusterrolebinding",
input: ` input: `
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: example.com/v1
kind: ClusterRoleBinding kind: ClusterRoleBinding
subjects: subjects:
- name: default - name: default
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: example.com/v1
kind: ClusterRoleBinding kind: ClusterRoleBinding
subjects: subjects:
- name: default - name: default
namespace: foo namespace: foo
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: example.com/v1
kind: ClusterRoleBinding kind: ClusterRoleBinding
subjects: subjects:
- name: something - name: something
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: example.com/v1
kind: ClusterRoleBinding kind: ClusterRoleBinding
subjects: subjects:
- name: something - name: something
namespace: foo namespace: foo
`, `,
expected: ` expected: `
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: example.com/v1
kind: ClusterRoleBinding kind: ClusterRoleBinding
subjects: subjects:
- name: default - name: default
namespace: bar namespace: bar
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: example.com/v1
kind: ClusterRoleBinding kind: ClusterRoleBinding
subjects: subjects:
- name: default - name: default
namespace: bar namespace: bar
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: example.com/v1
kind: ClusterRoleBinding kind: ClusterRoleBinding
subjects: subjects:
- name: something - name: something
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: example.com/v1
kind: ClusterRoleBinding kind: ClusterRoleBinding
subjects: subjects:
- name: something - name: something
@@ -286,91 +283,21 @@ a:
}, },
}, },
}, },
{
name: "mutation tracker",
input: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: instance
---
apiVersion: example.com/v1
kind: RoleBinding
subjects:
- name: default
`,
expected: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: instance
namespace: bar
a:
b:
c: bar
---
apiVersion: example.com/v1
kind: RoleBinding
subjects:
- name: default
namespace: bar
metadata:
namespace: bar
a:
b:
c: bar
`,
filter: namespace.Filter{Namespace: "bar"},
fsslice: []types.FieldSpec{
{
Path: "a/b/c",
CreateIfNotPresent: true,
},
},
mutationTracker: mutationTrackerStub.MutationTracker,
expectedSetValueArgs: []filtertest_test.SetValueArg{
{
Value: "bar",
NodePath: []string{"metadata", "namespace"},
},
{
Value: "bar",
NodePath: []string{"a", "b", "c"},
},
{
Value: "bar",
NodePath: []string{"metadata", "namespace"},
},
{
Value: "bar",
NodePath: []string{"namespace"},
},
{
Value: "bar",
NodePath: []string{"a", "b", "c"},
},
},
},
} }
type TestCase struct { type TestCase struct {
name string name string
input string input string
expected string expected string
filter namespace.Filter filter namespace.Filter
fsslice types.FsSlice fsslice types.FsSlice
mutationTracker func(key, value, tag string, node *yaml.RNode)
expectedSetValueArgs []filtertest_test.SetValueArg
} }
var config = builtinconfig.MakeDefaultConfig() var config = builtinconfig.MakeDefaultConfig()
func TestNamespace_Filter(t *testing.T) { func TestNamespace_Filter(t *testing.T) {
for i := range tests { for i := range tests {
mutationTrackerStub.Reset()
test := tests[i] test := tests[i]
test.filter.WithMutationTracker(test.mutationTracker)
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
test.filter.FsSlice = append(config.NameSpace, test.fsslice...) test.filter.FsSlice = append(config.NameSpace, test.fsslice...)
if !assert.Equal(t, if !assert.Equal(t,
@@ -379,7 +306,6 @@ func TestNamespace_Filter(t *testing.T) {
filtertest_test.RunFilter(t, test.input, test.filter))) { filtertest_test.RunFilter(t, test.input, test.filter))) {
t.FailNow() t.FailNow()
} }
assert.Equal(t, test.expectedSetValueArgs, mutationTrackerStub.SetValueArgs())
}) })
} }
} }

View File

@@ -15,22 +15,14 @@ type Filter struct {
var _ kio.Filter = Filter{} var _ kio.Filter = Filter{}
// Filter does a strategic merge patch, which can delete nodes.
func (pf Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { func (pf Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
var result []*yaml.RNode var result []*yaml.RNode
for i := range nodes { for i := range nodes {
r, err := merge2.Merge( r, err := merge2.Merge(pf.Patch, nodes[i])
pf.Patch, nodes[i],
yaml.MergeOptions{
ListIncreaseDirection: yaml.MergeOptionsListPrepend,
},
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if r != nil { result = append(result, r)
result = append(result, r)
}
} }
return result, nil return result, nil
} }

View File

@@ -15,101 +15,6 @@ func TestFilter(t *testing.T) {
patch *yaml.RNode patch *yaml.RNode
expected string expected string
}{ }{
"simple": {
input: `apiVersion: v1
kind: Deployment
metadata:
name: clown
spec:
numReplicas: 1
`,
patch: yaml.MustParse(`apiVersion: v1
kind: Deployment
metadata:
name: clown
spec:
numReplicas: 999
`),
expected: `apiVersion: v1
kind: Deployment
metadata:
name: clown
spec:
numReplicas: 999
`,
},
"nullMapEntry1": {
input: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: my-foo
spec:
bar:
B:
C: Z
`,
patch: yaml.MustParse(`
apiVersion: example.com/v1
kind: Foo
metadata:
name: my-foo
spec:
bar:
C: Z
D: W
baz:
hello: world
`),
expected: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: my-foo
spec:
bar:
C: Z
D: W
baz:
hello: world
`,
},
"nullMapEntry2": {
input: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: my-foo
spec:
bar:
C: Z
D: W
baz:
hello: world
`,
patch: yaml.MustParse(`
apiVersion: example.com/v1
kind: Foo
metadata:
name: my-foo
spec:
bar:
B:
C: Z
`),
expected: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: my-foo
spec:
bar:
C: Z
D: W
baz:
hello: world
`,
},
"simple patch": { "simple patch": {
input: ` input: `
apiVersion: apps/v1 apiVersion: apps/v1
@@ -162,10 +67,10 @@ spec:
template: template:
spec: spec:
containers: containers:
- name: foo0
- name: foo1 - name: foo1
- name: foo2 - name: foo2
- name: foo3 - name: foo3
- name: foo0
`, `,
}, },
"volumes patch": { "volumes patch": {
@@ -202,10 +107,10 @@ spec:
template: template:
spec: spec:
volumes: volumes:
- name: foo0
- name: foo1 - name: foo1
- name: foo2 - name: foo2
- name: foo3 - name: foo3
- name: foo0
`, `,
}, },
"nested patch": { "nested patch": {
@@ -237,498 +142,6 @@ spec:
- name: nginx - name: nginx
args: args:
- def - def
`,
},
"remove mapping - directive": {
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeploy
spec:
template:
spec:
containers:
- name: test
image: test
`,
patch: yaml.MustParse(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeploy
spec:
template:
spec:
containers:
- name: test
image: test
$patch: delete
`),
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeploy
spec:
template:
spec:
containers: []
`,
},
"replace mapping - directive": {
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeploy
spec:
template:
spec:
containers:
- name: test
image: test
`,
patch: yaml.MustParse(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeploy
spec:
template:
spec:
$patch: replace
containers:
- name: new
`),
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeploy
spec:
template:
spec:
containers:
- name: new
`,
},
"merge mapping - directive": {
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeploy
spec:
template:
spec:
containers:
- name: test
image: test
`,
patch: yaml.MustParse(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeploy
spec:
template:
spec:
containers:
- name: test
image: test1
$patch: merge
`),
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeploy
spec:
template:
spec:
containers:
- name: test
image: test1
`,
},
"remove list - directive": {
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeploy
spec:
template:
spec:
containers:
- name: test
image: test
`,
patch: yaml.MustParse(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeploy
spec:
template:
spec:
containers:
- whatever
- $patch: delete
`),
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeploy
spec:
template:
spec: {}
`,
},
"replace list - directive": {
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeploy
spec:
template:
spec:
containers:
- name: test
image: test
`,
patch: yaml.MustParse(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeploy
spec:
template:
spec:
containers:
- name: replace
image: replace
- $patch: replace
`),
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeploy
spec:
template:
spec:
containers:
- name: replace
image: replace
`,
},
"merge list - directive": {
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeploy
spec:
template:
spec:
containers:
- name: test
image: test
`,
patch: yaml.MustParse(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeploy
spec:
template:
spec:
containers:
- name: test2
image: test2
- $patch: merge
`),
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: myDeploy
spec:
template:
spec:
containers:
- name: test2
image: test2
- name: test
image: test
`,
},
"list map keys - add a port, no names": {
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deployment
spec:
template:
spec:
containers:
- image: test-image
name: test-deployment
ports:
- containerPort: 8080
protocol: TCP
`,
patch: yaml.MustParse(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deployment
spec:
template:
spec:
containers:
- image: test-image
name: test-deployment
ports:
- containerPort: 8080
protocol: UDP
- containerPort: 80
protocol: UDP
`),
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deployment
spec:
template:
spec:
containers:
- image: test-image
name: test-deployment
ports:
- containerPort: 8080
protocol: UDP
- containerPort: 80
protocol: UDP
- containerPort: 8080
protocol: TCP
`,
},
"list map keys - add name to port": {
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deployment
spec:
template:
spec:
containers:
- image: test-image
name: test-deployment
ports:
- containerPort: 8080
protocol: UDP
- containerPort: 8080
protocol: TCP
`,
patch: yaml.MustParse(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deployment
spec:
template:
spec:
containers:
- image: test-image
name: test-deployment
ports:
- containerPort: 8080
protocol: UDP
name: UDP-name-patch
`),
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deployment
spec:
template:
spec:
containers:
- image: test-image
name: test-deployment
ports:
- containerPort: 8080
protocol: UDP
name: UDP-name-patch
- containerPort: 8080
protocol: TCP
`,
},
"list map keys - replace port name": {
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deployment
spec:
template:
spec:
containers:
- image: test-image
name: test-deployment
ports:
- containerPort: 8080
protocol: UDP
name: UDP-name-original
- containerPort: 8080
protocol: TCP
name: TCP-name-original
`,
patch: yaml.MustParse(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deployment
spec:
template:
spec:
containers:
- image: test-image
name: test-deployment
ports:
- containerPort: 8080
protocol: UDP
name: UDP-name-patch
`),
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deployment
spec:
template:
spec:
containers:
- image: test-image
name: test-deployment
ports:
- containerPort: 8080
protocol: UDP
name: UDP-name-patch
- containerPort: 8080
protocol: TCP
name: TCP-name-original
`,
},
"list map keys - add a port, no protocol": {
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deployment
spec:
template:
spec:
containers:
- image: test-image
name: test-deployment
ports:
- containerPort: 8080
`,
patch: yaml.MustParse(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deployment
spec:
template:
spec:
containers:
- image: test-image
name: test-deployment
ports:
- containerPort: 80
`),
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deployment
spec:
template:
spec:
containers:
- image: test-image
name: test-deployment
ports:
- containerPort: 80
- containerPort: 8080
`,
},
// Test for issue #3513
// Currently broken; when one port has only containerPort, the output
// should not merge containerPort 8301 together
// This occurs because when protocol is missing on the first port,
// the merge code uses [containerPort] as the merge key rather than
// [containerPort, protocol]
"list map keys - protocol only present on some ports": {
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deployment
spec:
template:
spec:
containers:
- name: consul
image: "dashicorp/consul:1.9.1"
ports:
- containerPort: 8500
name: http
- containerPort: 8301
protocol: "TCP"
- containerPort: 8301
protocol: "UDP"
`,
patch: yaml.MustParse(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deployment
labels:
test: label
`),
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deployment
labels:
test: label
spec:
template:
spec:
containers:
- name: consul
image: "dashicorp/consul:1.9.1"
ports:
- containerPort: 8500
name: http
- containerPort: 8301
protocol: "TCP"
- containerPort: 8301
protocol: "UDP"
`, `,
}, },
} }

View File

@@ -1,6 +0,0 @@
// Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package prefix contains a kio.Filter implementation of the kustomize
// PrefixTransformer.
package prefix

View File

@@ -0,0 +1,6 @@
// Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package prefixsuffix contains a kio.Filter implementation of the kustomize
// PrefixSuffixTransformer.
package prefixsuffix

View File

@@ -1,14 +1,14 @@
// Copyright 2020 The Kubernetes Authors. // Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package prefix_test package prefixsuffix_test
import ( import (
"bytes" "bytes"
"log" "log"
"os" "os"
"sigs.k8s.io/kustomize/api/filters/prefix" "sigs.k8s.io/kustomize/api/filters/prefixsuffix"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/kio"
) )
@@ -26,7 +26,7 @@ kind: Bar
metadata: metadata:
name: instance name: instance
`)}}, `)}},
Filters: []kio.Filter{prefix.Filter{ Filters: []kio.Filter{prefixsuffix.Filter{
Prefix: "baz-", FieldSpec: types.FieldSpec{Path: "metadata/name"}}}, Prefix: "baz-", FieldSpec: types.FieldSpec{Path: "metadata/name"}}},
Outputs: []kio.Writer{kio.ByteWriter{Writer: os.Stdout}}, Outputs: []kio.Writer{kio.ByteWriter{Writer: os.Stdout}},
}.Execute() }.Execute()

View File

@@ -1,7 +1,7 @@
// Copyright 2019 The Kubernetes Authors. // Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package prefix package prefixsuffix
import ( import (
"fmt" "fmt"
@@ -13,22 +13,15 @@ import (
"sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml"
) )
// Filter applies resource name prefix's using the fieldSpecs // Filter applies resource name prefix's and suffix's using the fieldSpecs
type Filter struct { type Filter struct {
Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"` Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"`
Suffix string `json:"suffix,omitempty" yaml:"suffix,omitempty"`
FieldSpec types.FieldSpec `json:"fieldSpec,omitempty" yaml:"fieldSpec,omitempty"` FieldSpec types.FieldSpec `json:"fieldSpec,omitempty" yaml:"fieldSpec,omitempty"`
trackableSetter filtersutil.TrackableSetter
} }
var _ kio.Filter = Filter{} var _ kio.Filter = Filter{}
var _ kio.TrackableFilter = &Filter{}
// WithMutationTracker registers a callback which will be invoked each time a field is mutated
func (f *Filter) WithMutationTracker(callback func(key, value, tag string, node *yaml.RNode)) {
f.trackableSetter.WithMutationTracker(callback)
}
func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
return kio.FilterAll(yaml.FilterFunc(f.run)).Filter(nodes) return kio.FilterAll(yaml.FilterFunc(f.run)).Filter(nodes)
@@ -45,6 +38,6 @@ func (f Filter) run(node *yaml.RNode) (*yaml.RNode, error) {
} }
func (f Filter) evaluateField(node *yaml.RNode) error { func (f Filter) evaluateField(node *yaml.RNode) error {
return f.trackableSetter.SetScalar(fmt.Sprintf( return filtersutil.SetScalar(fmt.Sprintf(
"%s%s", f.Prefix, node.YNode().Value))(node) "%s%s%s", f.Prefix, node.YNode().Value, f.Suffix))(node)
} }

View File

@@ -1,21 +1,18 @@
// Copyright 2019 The Kubernetes Authors. // Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package prefix_test package prefixsuffix_test
import ( import (
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/api/filters/prefix" "sigs.k8s.io/kustomize/api/filters/prefixsuffix"
filtertest_test "sigs.k8s.io/kustomize/api/testutils/filtertest" filtertest_test "sigs.k8s.io/kustomize/api/testutils/filtertest"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/yaml"
) )
var mutationTrackerStub = filtertest_test.MutationTrackerStub{}
var tests = map[string]TestCase{ var tests = map[string]TestCase{
"prefix": { "prefix": {
input: ` input: `
@@ -40,10 +37,62 @@ kind: Bar
metadata: metadata:
name: foo-instance name: foo-instance
`, `,
filter: prefix.Filter{ filter: prefixsuffix.Filter{Prefix: "foo-"},
Prefix: "foo-", fs: types.FieldSpec{Path: "metadata/name"},
FieldSpec: types.FieldSpec{Path: "metadata/name"}, },
},
"suffix": {
input: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: instance
---
apiVersion: example.com/v1
kind: Bar
metadata:
name: instance
`,
expected: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: instance-foo
---
apiVersion: example.com/v1
kind: Bar
metadata:
name: instance-foo
`,
filter: prefixsuffix.Filter{Suffix: "-foo"},
fs: types.FieldSpec{Path: "metadata/name"},
},
"prefix-suffix": {
input: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: instance
---
apiVersion: example.com/v1
kind: Bar
metadata:
name: instance
`,
expected: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: bar-instance-foo
---
apiVersion: example.com/v1
kind: Bar
metadata:
name: bar-instance-foo
`,
filter: prefixsuffix.Filter{Prefix: "bar-", Suffix: "-foo"},
fs: types.FieldSpec{Path: "metadata/name"},
}, },
"data-fieldspecs": { "data-fieldspecs": {
@@ -81,74 +130,29 @@ a:
b: b:
c: foo-d c: foo-d
`, `,
filter: prefix.Filter{ filter: prefixsuffix.Filter{Prefix: "foo-"},
Prefix: "foo-", fs: types.FieldSpec{Path: "a/b/c"},
FieldSpec: types.FieldSpec{Path: "a/b/c"},
},
},
"mutation tracker": {
input: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: instance
---
apiVersion: example.com/v1
kind: Bar
metadata:
name: instance
`,
expected: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: foo-instance
---
apiVersion: example.com/v1
kind: Bar
metadata:
name: foo-instance
`,
filter: prefix.Filter{
Prefix: "foo-",
FieldSpec: types.FieldSpec{Path: "metadata/name"},
},
mutationTracker: mutationTrackerStub.MutationTracker,
expectedSetValueArgs: []filtertest_test.SetValueArg{
{
Value: "foo-instance",
NodePath: []string{"metadata", "name"},
},
{
Value: "foo-instance",
NodePath: []string{"metadata", "name"},
},
},
}, },
} }
type TestCase struct { type TestCase struct {
input string input string
expected string expected string
filter prefix.Filter filter prefixsuffix.Filter
mutationTracker func(key, value, tag string, node *yaml.RNode) fs types.FieldSpec
expectedSetValueArgs []filtertest_test.SetValueArg
} }
func TestFilter(t *testing.T) { func TestFilter(t *testing.T) {
for name := range tests { for name := range tests {
mutationTrackerStub.Reset()
test := tests[name] test := tests[name]
test.filter.WithMutationTracker(test.mutationTracker)
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
test.filter.FieldSpec = test.fs
if !assert.Equal(t, if !assert.Equal(t,
strings.TrimSpace(test.expected), strings.TrimSpace(test.expected),
strings.TrimSpace( strings.TrimSpace(
filtertest_test.RunFilter(t, test.input, test.filter))) { filtertest_test.RunFilter(t, test.input, test.filter))) {
t.FailNow() t.FailNow()
} }
assert.Equal(t, test.expectedSetValueArgs, mutationTrackerStub.SetValueArgs())
}) })
} }
} }

View File

@@ -1,3 +1,3 @@
// Package refvar contains a kio.Filter implementation of the kustomize // Package refvar contains a kio.Filter implementation of the kustomize
// refvar transformer (find and replace $(FOO) style variables in strings). // refvar transformer.
package refvar package refvar

View File

@@ -8,13 +8,15 @@ import (
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml"
expansion2 "sigs.k8s.io/kustomize/api/internal/accumulator/expansion"
) )
// Filter updates $(VAR) style variables with values. // Filter updates $(VAR) style variables with values.
// The fieldSpecs are the places to look for occurrences of $(VAR). // The fieldSpecs are the places to look for occurrences of $(VAR).
type Filter struct { type Filter struct {
MappingFunc MappingFunc `json:"mappingFunc,omitempty" yaml:"mappingFunc,omitempty"` MappingFunc func(string) interface{} `json:"mappingFunc,omitempty" yaml:"mappingFunc,omitempty"`
FieldSpec types.FieldSpec `json:"fieldSpec,omitempty" yaml:"fieldSpec,omitempty"` FieldSpec types.FieldSpec `json:"fieldSpec,omitempty" yaml:"fieldSpec,omitempty"`
} }
func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
@@ -47,21 +49,12 @@ func (f Filter) set(node *yaml.RNode) error {
func updateNodeValue(node *yaml.Node, newValue interface{}) { func updateNodeValue(node *yaml.Node, newValue interface{}) {
switch newValue := newValue.(type) { switch newValue := newValue.(type) {
case int:
node.Value = strconv.FormatInt(int64(newValue), 10)
node.Tag = yaml.NodeTagInt
case int32:
node.Value = strconv.FormatInt(int64(newValue), 10)
node.Tag = yaml.NodeTagInt
case int64: case int64:
node.Value = strconv.FormatInt(newValue, 10) node.Value = strconv.FormatInt(newValue, 10)
node.Tag = yaml.NodeTagInt node.Tag = yaml.NodeTagInt
case bool: case bool:
node.SetString(strconv.FormatBool(newValue)) node.SetString(strconv.FormatBool(newValue))
node.Tag = yaml.NodeTagBool node.Tag = yaml.NodeTagBool
case float32:
node.SetString(strconv.FormatFloat(float64(newValue), 'f', -1, 32))
node.Tag = yaml.NodeTagFloat
case float64: case float64:
node.SetString(strconv.FormatFloat(newValue, 'f', -1, 64)) node.SetString(strconv.FormatFloat(newValue, 'f', -1, 64))
node.Tag = yaml.NodeTagFloat node.Tag = yaml.NodeTagFloat
@@ -76,7 +69,7 @@ func (f Filter) setScalar(node *yaml.RNode) error {
if !yaml.IsYNodeString(node.YNode()) { if !yaml.IsYNodeString(node.YNode()) {
return nil return nil
} }
v := DoReplacements(node.YNode().Value, f.MappingFunc) v := expansion2.Expand(node.YNode().Value, f.MappingFunc)
updateNodeValue(node.YNode(), v) updateNodeValue(node.YNode(), v)
return nil return nil
} }
@@ -85,14 +78,12 @@ func (f Filter) setMap(node *yaml.RNode) error {
contents := node.YNode().Content contents := node.YNode().Content
for i := 0; i < len(contents); i += 2 { for i := 0; i < len(contents); i += 2 {
if !yaml.IsYNodeString(contents[i]) { if !yaml.IsYNodeString(contents[i]) {
return fmt.Errorf( return fmt.Errorf("invalid map key: %s, type: %s", contents[i].Value, contents[i].Tag)
"invalid map key: value='%s', tag='%s'",
contents[i].Value, contents[i].Tag)
} }
if !yaml.IsYNodeString(contents[i+1]) { if !yaml.IsYNodeString(contents[i+1]) {
continue continue
} }
newValue := DoReplacements(contents[i+1].Value, f.MappingFunc) newValue := expansion2.Expand(contents[i+1].Value, f.MappingFunc)
updateNodeValue(contents[i+1], newValue) updateNodeValue(contents[i+1], newValue)
} }
return nil return nil
@@ -103,7 +94,7 @@ func (f Filter) setSeq(node *yaml.RNode) error {
if !yaml.IsYNodeString(item) { if !yaml.IsYNodeString(item) {
return fmt.Errorf("invalid value type expect a string") return fmt.Errorf("invalid value type expect a string")
} }
newValue := DoReplacements(item.Value, f.MappingFunc) newValue := expansion2.Expand(item.Value, f.MappingFunc)
updateNodeValue(item, newValue) updateNodeValue(item, newValue)
} }
return nil return nil

View File

@@ -1,22 +1,18 @@
package refvar_test package refvar
import ( import (
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
. "sigs.k8s.io/kustomize/api/filters/refvar" expansion2 "sigs.k8s.io/kustomize/api/internal/accumulator/expansion"
filtertest_test "sigs.k8s.io/kustomize/api/testutils/filtertest" filtertest_test "sigs.k8s.io/kustomize/api/testutils/filtertest"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml"
) )
var makeMf = func(theMap map[string]interface{}) MappingFunc {
ignored := make(map[string]int)
return MakePrimitiveReplacer(ignored, theMap)
}
func TestFilter(t *testing.T) { func TestFilter(t *testing.T) {
replacementCounts := make(map[string]int)
testCases := map[string]struct { testCases := map[string]struct {
input string input string
@@ -39,7 +35,7 @@ metadata:
spec: spec:
replicas: 5`, replicas: 5`,
filter: Filter{ filter: Filter{
MappingFunc: makeMf(map[string]interface{}{ MappingFunc: expansion2.MappingFuncFor(replacementCounts, map[string]interface{}{
"VAR": int64(5), "VAR": int64(5),
}), }),
FieldSpec: types.FieldSpec{Path: "spec/replicas"}, FieldSpec: types.FieldSpec{Path: "spec/replicas"},
@@ -61,7 +57,7 @@ metadata:
spec: spec:
replicas: 1`, replicas: 1`,
filter: Filter{ filter: Filter{
MappingFunc: makeMf(map[string]interface{}{ MappingFunc: expansion2.MappingFuncFor(replacementCounts, map[string]interface{}{
"VAR": int64(5), "VAR": int64(5),
}), }),
FieldSpec: types.FieldSpec{Path: "spec/replicas"}, FieldSpec: types.FieldSpec{Path: "spec/replicas"},
@@ -83,7 +79,7 @@ metadata:
spec: spec:
replicas: 1`, replicas: 1`,
filter: Filter{ filter: Filter{
MappingFunc: makeMf(map[string]interface{}{ MappingFunc: expansion2.MappingFuncFor(replacementCounts, map[string]interface{}{
"VAR": int64(5), "VAR": int64(5),
}), }),
FieldSpec: types.FieldSpec{Path: "a/b/c"}, FieldSpec: types.FieldSpec{Path: "a/b/c"},
@@ -115,7 +111,7 @@ data:
- false - false
- 1.23`, - 1.23`,
filter: Filter{ filter: Filter{
MappingFunc: makeMf(map[string]interface{}{ MappingFunc: expansion2.MappingFuncFor(replacementCounts, map[string]interface{}{
"FOO": "foo", "FOO": "foo",
"BAR": "bar", "BAR": "bar",
"BOOL": false, "BOOL": false,
@@ -146,7 +142,7 @@ data:
BAZ: $(BAZ) BAZ: $(BAZ)
PLUS: foo+bar`, PLUS: foo+bar`,
filter: Filter{ filter: Filter{
MappingFunc: makeMf(map[string]interface{}{ MappingFunc: expansion2.MappingFuncFor(replacementCounts, map[string]interface{}{
"FOO": "foo", "FOO": "foo",
"BAR": "bar", "BAR": "bar",
}), }),
@@ -185,35 +181,13 @@ data:
SLICE: SLICE:
- $(FOO)`, - $(FOO)`,
filter: Filter{ filter: Filter{
MappingFunc: makeMf(map[string]interface{}{ MappingFunc: expansion2.MappingFuncFor(replacementCounts, map[string]interface{}{
"FOO": "foo", "FOO": "foo",
"BAR": "bar", "BAR": "bar",
}), }),
FieldSpec: types.FieldSpec{Path: "data/slice2"}, FieldSpec: types.FieldSpec{Path: "data/slice2"},
}, },
}, },
"null value": {
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: dep
data:
FOO: null`,
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: dep
data:
FOO: null`,
filter: Filter{
MappingFunc: makeMf(map[string]interface{}{
// no replacements!
}),
FieldSpec: types.FieldSpec{Path: "data/FOO"},
},
},
} }
for tn, tc := range testCases { for tn, tc := range testCases {
@@ -229,6 +203,8 @@ data:
} }
func TestFilterUnhappy(t *testing.T) { func TestFilterUnhappy(t *testing.T) {
replacementCounts := make(map[string]int)
testCases := map[string]struct { testCases := map[string]struct {
input string input string
expectedError string expectedError string
@@ -243,9 +219,18 @@ metadata:
data: data:
slice: slice:
- false`, - false`,
expectedError: `considering field 'data/slice' of object Deployment.v1.apps/dep.[noNs]: invalid value type expect a string`, expectedError: `obj 'apiVersion: apps/v1
kind: Deployment
metadata:
name: dep
annotations:
config.kubernetes.io/index: '0'
data:
slice:
- false
' at path 'data/slice': invalid value type expect a string`,
filter: Filter{ filter: Filter{
MappingFunc: makeMf(map[string]interface{}{ MappingFunc: expansion2.MappingFuncFor(replacementCounts, map[string]interface{}{
"VAR": int64(5), "VAR": int64(5),
}), }),
FieldSpec: types.FieldSpec{Path: "data/slice"}, FieldSpec: types.FieldSpec{Path: "data/slice"},
@@ -259,14 +244,36 @@ metadata:
name: dep name: dep
data: data:
1: str`, 1: str`,
expectedError: `considering field 'data' of object Deployment.v1.apps/dep.[noNs]: invalid map key: value='1', tag='` + yaml.NodeTagInt + `'`, expectedError: `obj 'apiVersion: apps/v1
kind: Deployment
metadata:
name: dep
annotations:
config.kubernetes.io/index: '0'
data:
1: str
' at path 'data': invalid map key: 1, type: ` + yaml.NodeTagInt,
filter: Filter{ filter: Filter{
MappingFunc: makeMf(map[string]interface{}{ MappingFunc: expansion2.MappingFuncFor(replacementCounts, map[string]interface{}{
"VAR": int64(5), "VAR": int64(5),
}), }),
FieldSpec: types.FieldSpec{Path: "data"}, FieldSpec: types.FieldSpec{Path: "data"},
}, },
}, },
"null input": {
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: dep
data:
FOO: null`,
expectedError: "obj '' at path 'data/FOO': invalid type encountered 0",
filter: Filter{
MappingFunc: expansion2.MappingFuncFor(replacementCounts, map[string]interface{}{}),
FieldSpec: types.FieldSpec{Path: "data/FOO"},
},
},
} }
for tn, tc := range testCases { for tn, tc := range testCases {

View File

@@ -1,4 +0,0 @@
// Package replacement contains a kio.Filter implementation of the kustomize
// replacement transformer (accepts sources and looks for targets to replace
// their values with values from the sources).
package replacement

View File

@@ -1,68 +0,0 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package replacement
import (
"bytes"
"log"
"os"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
func ExampleFilter() {
f := Filter{}
err := yaml.Unmarshal([]byte(`
replacements:
- source:
kind: Foo2
fieldPath: spec.replicas
targets:
- select:
kind: Foo1
fieldPaths:
- spec.replicas`), &f)
if err != nil {
log.Fatal(err)
}
err = kio.Pipeline{
Inputs: []kio.Reader{&kio.ByteReader{Reader: bytes.NewBufferString(`
apiVersion: example.com/v1
kind: Foo1
metadata:
name: instance
spec:
replicas: 3
---
apiVersion: example.com/v1
kind: Foo2
metadata:
name: instance
spec:
replicas: 99
`)}},
Filters: []kio.Filter{f},
Outputs: []kio.Writer{kio.ByteWriter{Writer: os.Stdout}},
}.Execute()
if err != nil {
log.Fatal(err)
}
// Output:
// apiVersion: example.com/v1
// kind: Foo1
// metadata:
// name: instance
// spec:
// replicas: 99
// ---
// apiVersion: example.com/v1
// kind: Foo2
// metadata:
// name: instance
// spec:
// replicas: 99
}

View File

@@ -1,221 +0,0 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package replacement
import (
"fmt"
"strings"
"sigs.k8s.io/kustomize/api/internal/utils"
"sigs.k8s.io/kustomize/api/resource"
"sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/resid"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
type Filter struct {
Replacements []types.Replacement `json:"replacements,omitempty" yaml:"replacements,omitempty"`
}
// Filter replaces values of targets with values from sources
func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
for _, r := range f.Replacements {
if r.Source == nil || r.Targets == nil {
return nil, fmt.Errorf("replacements must specify a source and at least one target")
}
value, err := getReplacement(nodes, &r)
if err != nil {
return nil, err
}
nodes, err = applyReplacement(nodes, value, r.Targets)
if err != nil {
return nil, err
}
}
return nodes, nil
}
func applyReplacement(nodes []*yaml.RNode, value *yaml.RNode, targets []*types.TargetSelector) ([]*yaml.RNode, error) {
for _, t := range targets {
if t.Select == nil {
return nil, fmt.Errorf("target must specify resources to select")
}
if len(t.FieldPaths) == 0 {
t.FieldPaths = []string{types.DefaultReplacementFieldPath}
}
for _, n := range nodes {
ids, err := utils.MakeResIds(n)
if err != nil {
return nil, err
}
// filter targets by label and annotation selectors
selectByAnnoAndLabel, err := selectByAnnoAndLabel(n, t)
if err != nil {
return nil, err
}
if !selectByAnnoAndLabel {
continue
}
// filter targets by matching resource IDs
for _, id := range ids {
if id.IsSelectedBy(t.Select.ResId) && !rejectId(t.Reject, &id) {
err := applyToNode(n, value, t)
if err != nil {
return nil, err
}
break
}
}
}
}
return nodes, nil
}
func selectByAnnoAndLabel(n *yaml.RNode, t *types.TargetSelector) (bool, error) {
if matchesSelect, err := matchesAnnoAndLabelSelector(n, t.Select); !matchesSelect || err != nil {
return false, err
}
for _, reject := range t.Reject {
if reject.AnnotationSelector == "" && reject.LabelSelector == "" {
continue
}
if m, err := matchesAnnoAndLabelSelector(n, reject); m || err != nil {
return false, err
}
}
return true, nil
}
func matchesAnnoAndLabelSelector(n *yaml.RNode, selector *types.Selector) (bool, error) {
r := resource.Resource{RNode: *n}
annoMatch, err := r.MatchesAnnotationSelector(selector.AnnotationSelector)
if err != nil {
return false, err
}
labelMatch, err := r.MatchesLabelSelector(selector.LabelSelector)
if err != nil {
return false, err
}
return annoMatch && labelMatch, nil
}
func rejectId(rejects []*types.Selector, id *resid.ResId) bool {
for _, r := range rejects {
if !r.ResId.IsEmpty() && id.IsSelectedBy(r.ResId) {
return true
}
}
return false
}
func applyToNode(node *yaml.RNode, value *yaml.RNode, target *types.TargetSelector) error {
for _, fp := range target.FieldPaths {
fieldPath := utils.SmarterPathSplitter(fp, ".")
var t *yaml.RNode
var err error
if target.Options != nil && target.Options.Create {
t, err = node.Pipe(yaml.LookupCreate(value.YNode().Kind, fieldPath...))
} else {
t, err = node.Pipe(yaml.Lookup(fieldPath...))
}
if err != nil {
return err
}
if t != nil {
if err = setTargetValue(target.Options, t, value); err != nil {
return err
}
}
}
return nil
}
func setTargetValue(options *types.FieldOptions, t *yaml.RNode, value *yaml.RNode) error {
value = value.Copy()
if options != nil && options.Delimiter != "" {
if t.YNode().Kind != yaml.ScalarNode {
return fmt.Errorf("delimiter option can only be used with scalar nodes")
}
tv := strings.Split(t.YNode().Value, options.Delimiter)
v := yaml.GetValue(value)
// TODO: Add a way to remove an element
switch {
case options.Index < 0: // prefix
tv = append([]string{v}, tv...)
case options.Index >= len(tv): // suffix
tv = append(tv, v)
default: // replace an element
tv[options.Index] = v
}
value.YNode().Value = strings.Join(tv, options.Delimiter)
}
t.SetYNode(value.YNode())
return nil
}
func getReplacement(nodes []*yaml.RNode, r *types.Replacement) (*yaml.RNode, error) {
source, err := selectSourceNode(nodes, r.Source)
if err != nil {
return nil, err
}
if r.Source.FieldPath == "" {
r.Source.FieldPath = types.DefaultReplacementFieldPath
}
fieldPath := utils.SmarterPathSplitter(r.Source.FieldPath, ".")
rn, err := source.Pipe(yaml.Lookup(fieldPath...))
if err != nil {
return nil, err
}
if rn.IsNilOrEmpty() {
return nil, fmt.Errorf("fieldPath `%s` is missing for replacement source %s", r.Source.FieldPath, r.Source.ResId)
}
return getRefinedValue(r.Source.Options, rn)
}
func getRefinedValue(options *types.FieldOptions, rn *yaml.RNode) (*yaml.RNode, error) {
if options == nil || options.Delimiter == "" {
return rn, nil
}
if rn.YNode().Kind != yaml.ScalarNode {
return nil, fmt.Errorf("delimiter option can only be used with scalar nodes")
}
value := strings.Split(yaml.GetValue(rn), options.Delimiter)
if options.Index >= len(value) || options.Index < 0 {
return nil, fmt.Errorf("options.index %d is out of bounds for value %s", options.Index, yaml.GetValue(rn))
}
n := rn.Copy()
n.YNode().Value = value[options.Index]
return n, nil
}
// selectSourceNode finds the node that matches the selector, returning
// an error if multiple or none are found
func selectSourceNode(nodes []*yaml.RNode, selector *types.SourceSelector) (*yaml.RNode, error) {
var matches []*yaml.RNode
for _, n := range nodes {
ids, err := utils.MakeResIds(n)
if err != nil {
return nil, err
}
for _, id := range ids {
if id.IsSelectedBy(selector.ResId) {
if len(matches) > 0 {
return nil, fmt.Errorf(
"multiple matches for selector %s", selector)
}
matches = append(matches, n)
break
}
}
}
if len(matches) == 0 {
return nil, fmt.Errorf("nothing selected by %s", selector)
}
return matches[0], nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -14,17 +14,9 @@ import (
type Filter struct { type Filter struct {
Replica types.Replica `json:"replica,omitempty" yaml:"replica,omitempty"` Replica types.Replica `json:"replica,omitempty" yaml:"replica,omitempty"`
FieldSpec types.FieldSpec `json:"fieldSpec,omitempty" yaml:"fieldSpec,omitempty"` FieldSpec types.FieldSpec `json:"fieldSpec,omitempty" yaml:"fieldSpec,omitempty"`
trackableSetter filtersutil.TrackableSetter
} }
var _ kio.Filter = Filter{} var _ kio.Filter = Filter{}
var _ kio.TrackableFilter = &Filter{}
// WithMutationTracker registers a callback which will be invoked each time a field is mutated
func (rc *Filter) WithMutationTracker(callback func(key, value, tag string, node *yaml.RNode)) {
rc.trackableSetter.WithMutationTracker(callback)
}
func (rc Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { func (rc Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
return kio.FilterAll(yaml.FilterFunc(rc.run)).Filter(nodes) return kio.FilterAll(yaml.FilterFunc(rc.run)).Filter(nodes)
@@ -41,5 +33,5 @@ func (rc Filter) run(node *yaml.RNode) (*yaml.RNode, error) {
} }
func (rc Filter) set(node *yaml.RNode) error { func (rc Filter) set(node *yaml.RNode) error {
return rc.trackableSetter.SetScalar(strconv.FormatInt(rc.Replica.Count, 10))(node) return filtersutil.SetScalar(strconv.FormatInt(rc.Replica.Count, 10))(node)
} }

View File

@@ -7,17 +7,14 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
filtertest_test "sigs.k8s.io/kustomize/api/testutils/filtertest" filtertest_test "sigs.k8s.io/kustomize/api/testutils/filtertest"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/yaml"
) )
func TestFilter(t *testing.T) { func TestFilter(t *testing.T) {
mutationTrackerStub := filtertest_test.MutationTrackerStub{}
testCases := map[string]struct { testCases := map[string]struct {
input string input string
expected string expected string
filter Filter filter Filter
mutationTracker func(key, value, tag string, node *yaml.RNode)
expectedSetValueArgs []filtertest_test.SetValueArg
}{ }{
"update field": { "update field": {
input: ` input: `
@@ -164,43 +161,9 @@ spec:
FieldSpec: types.FieldSpec{Path: "spec/template/replicas"}, FieldSpec: types.FieldSpec{Path: "spec/template/replicas"},
}, },
}, },
"mutation tracker": {
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: dep
spec:
replicas: 5
`,
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: dep
spec:
replicas: 42
`,
filter: Filter{
Replica: types.Replica{
Name: "dep",
Count: 42,
},
FieldSpec: types.FieldSpec{Path: "spec/replicas"},
},
mutationTracker: mutationTrackerStub.MutationTracker,
expectedSetValueArgs: []filtertest_test.SetValueArg{
{
Value: "42",
NodePath: []string{"spec", "replicas"},
},
},
},
} }
for tn, tc := range testCases { for tn, tc := range testCases {
mutationTrackerStub.Reset()
tc.filter.WithMutationTracker(tc.mutationTracker)
t.Run(tn, func(t *testing.T) { t.Run(tn, func(t *testing.T) {
if !assert.Equal(t, if !assert.Equal(t,
strings.TrimSpace(tc.expected), strings.TrimSpace(tc.expected),
@@ -208,7 +171,6 @@ spec:
filtertest_test.RunFilter(t, tc.input, tc.filter))) { filtertest_test.RunFilter(t, tc.input, tc.filter))) {
t.FailNow() t.FailNow()
} }
assert.Equal(t, tc.expectedSetValueArgs, mutationTrackerStub.SetValueArgs())
}) })
} }
} }

View File

@@ -1,6 +0,0 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package suffix contains a kio.Filter implementation of the kustomize
// SuffixTransformer.
package suffix

View File

@@ -1,47 +0,0 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package suffix_test
import (
"bytes"
"log"
"os"
"sigs.k8s.io/kustomize/api/filters/suffix"
"sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/kio"
)
func ExampleFilter() {
err := kio.Pipeline{
Inputs: []kio.Reader{&kio.ByteReader{Reader: bytes.NewBufferString(`
apiVersion: example.com/v1
kind: Foo
metadata:
name: instance
---
apiVersion: example.com/v1
kind: Bar
metadata:
name: instance
`)}},
Filters: []kio.Filter{suffix.Filter{
Suffix: "-baz", FieldSpec: types.FieldSpec{Path: "metadata/name"}}},
Outputs: []kio.Writer{kio.ByteWriter{Writer: os.Stdout}},
}.Execute()
if err != nil {
log.Fatal(err)
}
// Output:
// apiVersion: example.com/v1
// kind: Foo
// metadata:
// name: instance-baz
// ---
// apiVersion: example.com/v1
// kind: Bar
// metadata:
// name: instance-baz
}

View File

@@ -1,50 +0,0 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package suffix
import (
"fmt"
"sigs.k8s.io/kustomize/api/filters/fieldspec"
"sigs.k8s.io/kustomize/api/filters/filtersutil"
"sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// Filter applies resource name suffix's using the fieldSpecs
type Filter struct {
Suffix string `json:"suffix,omitempty" yaml:"suffix,omitempty"`
FieldSpec types.FieldSpec `json:"fieldSpec,omitempty" yaml:"fieldSpec,omitempty"`
trackableSetter filtersutil.TrackableSetter
}
var _ kio.Filter = Filter{}
var _ kio.TrackableFilter = &Filter{}
// WithMutationTracker registers a callback which will be invoked each time a field is mutated
func (f *Filter) WithMutationTracker(callback func(key, value, tag string, node *yaml.RNode)) {
f.trackableSetter.WithMutationTracker(callback)
}
func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
return kio.FilterAll(yaml.FilterFunc(f.run)).Filter(nodes)
}
func (f Filter) run(node *yaml.RNode) (*yaml.RNode, error) {
err := node.PipeE(fieldspec.Filter{
FieldSpec: f.FieldSpec,
SetValue: f.evaluateField,
CreateKind: yaml.ScalarNode, // Name is a ScalarNode
CreateTag: yaml.NodeTagString,
})
return node, err
}
func (f Filter) evaluateField(node *yaml.RNode) error {
return f.trackableSetter.SetScalar(fmt.Sprintf(
"%s%s", node.YNode().Value, f.Suffix))(node)
}

View File

@@ -1,154 +0,0 @@
// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package suffix_test
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/api/filters/suffix"
filtertest_test "sigs.k8s.io/kustomize/api/testutils/filtertest"
"sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
var mutationTrackerStub = filtertest_test.MutationTrackerStub{}
var tests = map[string]TestCase{
"suffix": {
input: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: instance
---
apiVersion: example.com/v1
kind: Bar
metadata:
name: instance
`,
expected: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: instance-foo
---
apiVersion: example.com/v1
kind: Bar
metadata:
name: instance-foo
`,
filter: suffix.Filter{
Suffix: "-foo",
FieldSpec: types.FieldSpec{Path: "metadata/name"},
},
},
"data-fieldspecs": {
input: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: instance
a:
b:
c: d
---
apiVersion: example.com/v1
kind: Bar
metadata:
name: instance
a:
b:
c: d
`,
expected: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: instance
a:
b:
c: d-foo
---
apiVersion: example.com/v1
kind: Bar
metadata:
name: instance
a:
b:
c: d-foo
`,
filter: suffix.Filter{
Suffix: "-foo",
FieldSpec: types.FieldSpec{Path: "a/b/c"},
},
},
"mutation tracker": {
input: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: instance
---
apiVersion: example.com/v1
kind: Bar
metadata:
name: instance
`,
expected: `
apiVersion: example.com/v1
kind: Foo
metadata:
name: instance-foo
---
apiVersion: example.com/v1
kind: Bar
metadata:
name: instance-foo
`,
filter: suffix.Filter{
Suffix: "-foo",
FieldSpec: types.FieldSpec{Path: "metadata/name"},
},
mutationTracker: mutationTrackerStub.MutationTracker,
expectedSetValueArgs: []filtertest_test.SetValueArg{
{
Value: "instance-foo",
NodePath: []string{"metadata", "name"},
},
{
Value: "instance-foo",
NodePath: []string{"metadata", "name"},
},
},
},
}
type TestCase struct {
input string
expected string
filter suffix.Filter
mutationTracker func(key, value, tag string, node *yaml.RNode)
expectedSetValueArgs []filtertest_test.SetValueArg
}
func TestFilter(t *testing.T) {
for name := range tests {
mutationTrackerStub.Reset()
test := tests[name]
test.filter.WithMutationTracker(test.mutationTracker)
t.Run(name, func(t *testing.T) {
if !assert.Equal(t,
strings.TrimSpace(test.expected),
strings.TrimSpace(
filtertest_test.RunFilter(t, test.input, test.filter))) {
t.FailNow()
}
assert.Equal(t, test.expectedSetValueArgs, mutationTrackerStub.SetValueArgs())
})
}
}

View File

@@ -6,7 +6,7 @@ package valueadd
import ( import (
"strings" "strings"
"sigs.k8s.io/kustomize/kyaml/filesys" "sigs.k8s.io/kustomize/api/filesys"
"sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml"
) )
@@ -98,17 +98,7 @@ var _ kio.Filter = Filter{}
func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
_, err := kio.FilterAll(yaml.FilterFunc( _, err := kio.FilterAll(yaml.FilterFunc(
func(node *yaml.RNode) (*yaml.RNode, error) { func(node *yaml.RNode) (*yaml.RNode, error) {
var fields []string fields := strings.Split(f.FieldPath, "/")
// if there is forward slash '/' in the field name, a back slash '\'
// will be used to escape it.
for _, f := range strings.Split(f.FieldPath, "/") {
if len(fields) > 0 && strings.HasSuffix(fields[len(fields)-1], "\\") {
concatField := strings.TrimSuffix(fields[len(fields)-1], "\\") + "/" + f
fields = append(fields[:len(fields)-1], concatField)
} else {
fields = append(fields, f)
}
}
// TODO: support SequenceNode. // TODO: support SequenceNode.
// Presumably here one could look for array indices (digits) at // Presumably here one could look for array indices (digits) at
// the end of the field path (as described in IETF RFC 6902 JSON), // the end of the field path (as described in IETF RFC 6902 JSON),

View File

@@ -94,20 +94,6 @@ spec:
FilePathPosition: 2, FilePathPosition: 2,
}, },
}, },
"backSlash": {
input: `
kind: SomeKind
`,
expectedOutput: `
kind: SomeKind
spec:
resourceRef/external: valueAdded
`,
filter: Filter{
Value: "valueAdded",
FieldPath: "spec/resourceRef\\/external",
},
},
} }
for tn, tc := range testCases { for tn, tc := range testCases {

View File

@@ -1,16 +1,24 @@
module sigs.k8s.io/kustomize/api module sigs.k8s.io/kustomize/api
go 1.16 go 1.14
require ( require (
github.com/evanphx/json-patch v4.11.0+incompatible github.com/evanphx/json-patch v4.5.0+incompatible
github.com/go-errors/errors v1.0.1 github.com/go-openapi/spec v0.19.5
github.com/golangci/golangci-lint v1.21.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/imdario/mergo v0.3.5 github.com/pkg/errors v0.8.1
github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.4.0
github.com/stretchr/testify v1.7.0 github.com/yujunz/go-getter v1.4.1-lite
gopkg.in/yaml.v2 v2.4.0 golang.org/x/tools v0.0.0-20191010075000-0337d82405ff
k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e gopkg.in/yaml.v2 v2.3.0
sigs.k8s.io/kustomize/kyaml v0.13.3 gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71
k8s.io/api v0.17.0
k8s.io/apimachinery v0.17.0
k8s.io/client-go v0.17.0
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a
sigs.k8s.io/kustomize/kyaml v0.6.1
sigs.k8s.io/yaml v1.2.0 sigs.k8s.io/yaml v1.2.0
) )
replace sigs.k8s.io/kustomize/kyaml v0.6.1 => ../kyaml

File diff suppressed because it is too large Load Diff

View File

@@ -20,12 +20,12 @@ func SortArrayAndComputeHash(s []string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
return encode(hex256(string(data))) return Encode(Hash(string(data)))
} }
// Copied from https://github.com/kubernetes/kubernetes // Copied from https://github.com/kubernetes/kubernetes
// /blob/master/pkg/kubectl/util/hash/hash.go // /blob/master/pkg/kubectl/util/hash/hash.go
func encode(hex string) (string, error) { func Encode(hex string) (string, error) {
if len(hex) < 10 { if len(hex) < 10 {
return "", fmt.Errorf( return "", fmt.Errorf(
"input length must be at least 10") "input length must be at least 10")
@@ -48,18 +48,23 @@ func encode(hex string) (string, error) {
return string(enc), nil return string(enc), nil
} }
// hex256 returns the hex form of the sha256 of the argument. // Hash returns the hex form of the sha256 of the argument.
func hex256(data string) string { func Hash(data string) string {
return fmt.Sprintf("%x", sha256.Sum256([]byte(data))) return fmt.Sprintf("%x", sha256.Sum256([]byte(data)))
} }
// Hasher computes the hash of an RNode. // HashRNode returns the hash value of input RNode
type Hasher struct{} func HashRNode(node *yaml.RNode) (string, error) {
// get node kind
kindNode, err := node.Pipe(yaml.FieldMatcher{Name: "kind"})
if err != nil {
return "", err
}
kind := kindNode.YNode().Value
// Hash returns a hash of the argument. // calculate hash for different kinds
func (h *Hasher) Hash(node *yaml.RNode) (r string, err error) { encoded := ""
var encoded string switch kind {
switch node.GetKind() {
case "ConfigMap": case "ConfigMap":
encoded, err = encodeConfigMap(node) encoded, err = encodeConfigMap(node)
case "Secret": case "Secret":
@@ -72,11 +77,10 @@ func (h *Hasher) Hash(node *yaml.RNode) (r string, err error) {
if err != nil { if err != nil {
return "", err return "", err
} }
return encode(hex256(encoded)) return Encode(Hash(encoded))
} }
func getNodeValues( func getNodeValues(node *yaml.RNode, paths []string) (map[string]interface{}, error) {
node *yaml.RNode, paths []string) (map[string]interface{}, error) {
values := make(map[string]interface{}) values := make(map[string]interface{})
for _, p := range paths { for _, p := range paths {
vn, err := node.Pipe(yaml.Lookup(p)) vn, err := node.Pipe(yaml.Lookup(p))
@@ -113,11 +117,8 @@ func encodeConfigMap(node *yaml.RNode) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
m := map[string]interface{}{ m := map[string]interface{}{"kind": "ConfigMap", "name": values["metadata/name"],
"kind": "ConfigMap", "data": values["data"]}
"name": values["metadata/name"],
"data": values["data"],
}
if _, ok := values["binaryData"].(map[string]interface{}); ok { if _, ok := values["binaryData"].(map[string]interface{}); ok {
m["binaryData"] = values["binaryData"] m["binaryData"] = values["binaryData"]
} }

View File

@@ -32,10 +32,10 @@ func TestSortArrayAndComputeHash(t *testing.T) {
} }
} }
func Test_hex256(t *testing.T) { func TestHash(t *testing.T) {
// hash the empty string to be sure that sha256 is being used // hash the empty string to be sure that sha256 is being used
expect := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" expect := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
sum := hex256("") sum := Hash("")
if expect != sum { if expect != sum {
t.Errorf("expected hash %q but got %q", expect, sum) t.Errorf("expected hash %q but got %q", expect, sum)
} }
@@ -56,7 +56,7 @@ kind: ConfigMap`, "6ct58987ht", ""},
{"one key", ` {"one key", `
apiVersion: v1 apiVersion: v1
kind: ConfigMap kind: ConfigMap
data: data:
one: ""`, "9g67k2htb6", ""}, one: ""`, "9g67k2htb6", ""},
// three keys (tests sorting order) // three keys (tests sorting order)
{"three keys", ` {"three keys", `
@@ -93,17 +93,17 @@ data:
binaryData: binaryData:
two: ""`, "698h7c7t9m", ""}, two: ""`, "698h7c7t9m", ""},
} }
h := &Hasher{}
for _, c := range cases { for _, c := range cases {
node, err := yaml.Parse(c.cmYaml) node, err := yaml.Parse(c.cmYaml)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
hashed, err := h.Hash(node) h, err := HashRNode(node)
if SkipRest(t, c.desc, err, c.err) { if SkipRest(t, c.desc, err, c.err) {
continue continue
} }
if c.hash != hashed { if c.hash != h {
t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h) t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h)
} }
} }
@@ -154,34 +154,35 @@ type: my-type
data: data:
one: ""`, "74bd68bm66", ""}, one: ""`, "74bd68bm66", ""},
} }
h := &Hasher{}
for _, c := range cases { for _, c := range cases {
node, err := yaml.Parse(c.secretYaml) node, err := yaml.Parse(c.secretYaml)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
hashed, err := h.Hash(node) h, err := HashRNode(node)
if SkipRest(t, c.desc, err, c.err) { if SkipRest(t, c.desc, err, c.err) {
continue continue
} }
if c.hash != hashed { if c.hash != h {
t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h) t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h)
} }
} }
} }
func TestBasicHash(t *testing.T) { func TestUnstructuredHash(t *testing.T) {
cases := map[string]struct { cases := []struct {
res string desc string
hash string unstructured string
err string hash string
err string
}{ }{
"minimal": {` {"minimal", `
apiVersion: test/v1 apiVersion: test/v1
kind: TestResource kind: TestResource
metadata: metadata:
name: my-resource`, "244782mkb7", ""}, name: my-resource`, "244782mkb7", ""},
"with spec": {` {"with spec", `
apiVersion: test/v1 apiVersion: test/v1
kind: TestResource kind: TestResource
metadata: metadata:
@@ -190,22 +191,19 @@ spec:
foo: 1 foo: 1
bar: abc`, "59m2mdccg4", ""}, bar: abc`, "59m2mdccg4", ""},
} }
h := &Hasher{}
for n := range cases { for _, c := range cases {
c := cases[n] node, err := yaml.Parse(c.unstructured)
t.Run(n, func(t *testing.T) { if err != nil {
node, err := yaml.Parse(c.res) t.Fatal(err)
if err != nil { }
t.Fatal(err) h, err := HashRNode(node)
} if SkipRest(t, c.desc, err, c.err) {
hashed, err := h.Hash(node) continue
if SkipRest(t, n, err, c.err) { }
return if c.hash != h {
} t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h)
if c.hash != hashed { }
t.Errorf("case %q, expect hash %q but got %q", n, c.hash, h)
}
})
} }
} }
@@ -224,7 +222,7 @@ kind: ConfigMap`, `{"data":"","kind":"ConfigMap","name":""}`, ""},
{"one key", ` {"one key", `
apiVersion: v1 apiVersion: v1
kind: ConfigMap kind: ConfigMap
data: data:
one: ""`, `{"data":{"one":""},"kind":"ConfigMap","name":""}`, ""}, one: ""`, `{"data":{"one":""},"kind":"ConfigMap","name":""}`, ""},
// three keys (tests sorting order) // three keys (tests sorting order)
{"three keys", ` {"three keys", `
@@ -336,10 +334,9 @@ data:
} }
} }
// SkipRest returns true if there was a non-nil error or if we expected an // SkipRest returns true if there was a non-nil error or if we expected an error that didn't happen,
// error that didn't happen, and logs the appropriate error on the test object. // and logs the appropriate error on the test object.
// The return value indicates whether we should skip the rest of the test case // The return value indicates whether we should skip the rest of the test case due to the error result.
// due to the error result.
func SkipRest(t *testing.T, desc string, err error, contains string) bool { func SkipRest(t *testing.T, desc string, err error, contains string) bool {
if err != nil { if err != nil {
if len(contains) == 0 { if len(contains) == 0 {

View File

@@ -5,8 +5,8 @@
package ifc package ifc
import ( import (
"sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/yaml"
) )
// Validator provides functions to validate annotations and labels // Validator provides functions to validate annotations and labels
@@ -38,10 +38,84 @@ type Loader interface {
Cleanup() error Cleanup() error
} }
// KustHasher returns a hash of the argument // Kunstructured represents a Kubernetes Resource Model object.
type Kunstructured interface {
// Several uses.
Copy() Kunstructured
// Used by Resource.Replace, which in turn is used in many places, e.g.
// - resource.Resource.Merge
// - resWrangler.appendReplaceOrMerge (AbsorbAll)
// - api.internal.k8sdeps.transformer.patch.conflictdetector
GetAnnotations() map[string]string
// Used by ResAccumulator and ReplacementTransformer.
GetFieldValue(string) (interface{}, error)
// Used by Resource.OrgId
GetGvk() resid.Gvk
// Used by resource.Factory.SliceFromBytes
GetKind() string
// Used by Resource.Replace
GetLabels() map[string]string
// Used by Resource.CurId and resource factory.
GetName() string
// Used by special case code in
// ResMap.SubsetThatCouldBeReferencedByResource
GetSlice(path string) ([]interface{}, error)
// GetString returns the value of a string field.
// Used by Resource.GetNamespace
GetString(string) (string, error)
// Several uses.
Map() map[string]interface{}
// Used by Resource.AsYAML and Resource.String
MarshalJSON() ([]byte, error)
// Used by resWrangler.Select
MatchesAnnotationSelector(selector string) (bool, error)
// Used by resWrangler.Select
MatchesLabelSelector(selector string) (bool, error)
// Used by Resource.Replace.
SetAnnotations(map[string]string)
// Used by PatchStrategicMergeTransformer.
SetGvk(resid.Gvk)
// Used by Resource.Replace and used to remove "validated by" labels.
SetLabels(map[string]string)
// Used by Resource.Replace.
SetName(string)
// Used by Resource.Replace.
SetNamespace(string)
// Needed, for now, by kyaml/filtersutil.ApplyToJSON.
UnmarshalJSON([]byte) error
}
// KunstructuredFactory makes instances of Kunstructured.
type KunstructuredFactory interface {
SliceFromBytes([]byte) ([]Kunstructured, error)
FromMap(m map[string]interface{}) Kunstructured
Hasher() KunstructuredHasher
MakeConfigMap(kvLdr KvLoader, args *types.ConfigMapArgs) (Kunstructured, error)
MakeSecret(kvLdr KvLoader, args *types.SecretArgs) (Kunstructured, error)
}
// KunstructuredHasher returns a hash of the argument
// or an error. // or an error.
type KustHasher interface { type KunstructuredHasher interface {
Hash(*yaml.RNode) (string, error) Hash(Kunstructured) (string, error)
} }
// See core.v1.SecretTypeOpaque // See core.v1.SecretTypeOpaque

View File

@@ -1,12 +1,12 @@
// Copyright 2019 The Kubernetes Authors. // Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package refvar // Package expansion provides functions find and replace $(FOO) style variables in strings.
package expansion
import ( import (
"bytes"
"fmt" "fmt"
"log"
"strings"
) )
const ( const (
@@ -17,64 +17,38 @@ const (
// syntaxWrap returns the input string wrapped by the expansion syntax. // syntaxWrap returns the input string wrapped by the expansion syntax.
func syntaxWrap(input string) string { func syntaxWrap(input string) string {
var sb strings.Builder return string(operator) + string(referenceOpener) + input + string(referenceCloser)
sb.WriteByte(operator)
sb.WriteByte(referenceOpener)
sb.WriteString(input)
sb.WriteByte(referenceCloser)
return sb.String()
} }
// MappingFunc maps a string to anything. // MappingFuncFor returns a mapping function for use with Expand that
type MappingFunc func(string) interface{} // implements the expansion semantics defined in the expansion spec; it
// returns the input string wrapped in the expansion syntax if no mapping
// MakePrimitiveReplacer returns a MappingFunc that uses a map to do // for the input is found.
// replacements, and a histogram to count map hits. func MappingFuncFor(
// counts map[string]int,
// Func behavior: context ...map[string]interface{}) func(string) interface{} {
// return func(input string) interface{} {
// If the input key is NOT found in the map, the key is wrapped up as for _, vars := range context {
// as a variable declaration string and returned, e.g. key FOO becomes $(FOO). val, ok := vars[input]
// This string is presumably put back where it was found, and might get replaced if ok {
// later. counts[input]++
// switch typedV := val.(type) {
// If the key is found in the map, the value is returned if it is a primitive case string, int64, float64, bool:
// type (string, bool, number), and the hit is counted. return typedV
// default:
// If it's not a primitive type (e.g. a map, struct, func, etc.) then this return syntaxWrap(input)
// function doesn't know what to do with it and it returns the key wrapped up }
// again as if it had not been replaced. This should probably be an error.
func MakePrimitiveReplacer(
counts map[string]int, someMap map[string]interface{}) MappingFunc {
return func(key string) interface{} {
if value, ok := someMap[key]; ok {
switch typedV := value.(type) {
case string, int, int32, int64, float32, float64, bool:
counts[key]++
return typedV
default:
// If the value is some complicated type (e.g. a map or struct),
// this function doesn't know how to jam it into a string,
// so just pretend it was a cache miss.
// Likely this should be an error instead of a silent failure,
// since the programmer passed an impossible value.
log.Printf(
"MakePrimitiveReplacer: bad replacement type=%T val=%v",
typedV, typedV)
return syntaxWrap(key)
} }
} }
// If unable to return the mapped variable, return it return syntaxWrap(input)
// as it was found, and a later mapping might be able to
// replace it.
return syntaxWrap(key)
} }
} }
// DoReplacements replaces variable references in the input string // Expand replaces variable references in the input string according to
// using the mapping function. // the expansion spec using the given mapping function to resolve the
func DoReplacements(input string, mapping MappingFunc) interface{} { // values of variables.
var buf strings.Builder func Expand(input string, mapping func(string) interface{}) interface{} {
var buf bytes.Buffer
checkpoint := 0 checkpoint := 0
for cursor := 0; cursor < len(input); cursor++ { for cursor := 0; cursor < len(input); cursor++ {
if input[cursor] == operator && cursor+1 < len(input) { if input[cursor] == operator && cursor+1 < len(input) {

View File

@@ -1,14 +1,13 @@
// Copyright 2019 The Kubernetes Authors. // Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package refvar_test package expansion_test
import ( import (
"fmt" "fmt"
"testing" "testing"
"github.com/stretchr/testify/assert" . "sigs.k8s.io/kustomize/api/internal/accumulator/expansion"
. "sigs.k8s.io/kustomize/api/filters/refvar"
) )
type expected struct { type expected struct {
@@ -16,48 +15,6 @@ type expected struct {
edited string edited string
} }
func TestPrimitiveReplacer(t *testing.T) {
varCounts := make(map[string]int)
f := MakePrimitiveReplacer(
varCounts,
map[string]interface{}{
"FOO": "bar",
"ZOO": "$(FOO)-1",
"BLU": "$(ZOO)-2",
"EIGHT": 8,
"PI": 3.14159,
"ZINT": "$(INT)",
"BOOL": "true",
"HUGENUMBER": int64(9223372036854775807),
"CRAZYMAP": map[string]int{"crazy": 200},
"ZBOOL": "$(BOOL)",
})
assert.Equal(t, "$()", f(""))
assert.Equal(t, "$( )", f(" "))
assert.Equal(t, "$(florida)", f("florida"))
assert.Equal(t, "$(0)", f("0"))
assert.Equal(t, "bar", f("FOO"))
assert.Equal(t, "bar", f("FOO"))
assert.Equal(t, "bar", f("FOO"))
assert.Equal(t, 8, f("EIGHT"))
assert.Equal(t, 8, f("EIGHT"))
assert.Equal(t, 3.14159, f("PI"))
assert.Equal(t, "true", f("BOOL"))
assert.Equal(t, int64(9223372036854775807), f("HUGENUMBER"))
assert.Equal(t, "$(FOO)-1", f("ZOO"))
assert.Equal(t, "$(CRAZYMAP)", f("CRAZYMAP"))
assert.Equal(t,
map[string]int{
"FOO": 3,
"EIGHT": 2,
"BOOL": 1,
"PI": 1,
"ZOO": 1,
"HUGENUMBER": 1,
},
varCounts)
}
func TestMapReference(t *testing.T) { func TestMapReference(t *testing.T) {
type env struct { type env struct {
Name string Name string
@@ -94,7 +51,7 @@ func TestMapReference(t *testing.T) {
}, },
} }
varMap := map[string]interface{}{ declaredEnv := map[string]interface{}{
"FOO": "bar", "FOO": "bar",
"ZOO": "$(FOO)-1", "ZOO": "$(FOO)-1",
"BLU": "$(ZOO)-2", "BLU": "$(ZOO)-2",
@@ -104,11 +61,11 @@ func TestMapReference(t *testing.T) {
"ZBOOL": "$(BOOL)", "ZBOOL": "$(BOOL)",
} }
varCounts := make(map[string]int) counts := make(map[string]int)
mapping := MappingFuncFor(counts, declaredEnv)
for _, env := range envs { for _, env := range envs {
varMap[env.Name] = DoReplacements( declaredEnv[env.Name] = Expand(fmt.Sprintf("%v", env.Value), mapping)
fmt.Sprintf("%v", env.Value),
MakePrimitiveReplacer(varCounts, varMap))
} }
expectedEnv := map[string]expected{ expectedEnv := map[string]expected{
@@ -122,20 +79,45 @@ func TestMapReference(t *testing.T) {
} }
for k, v := range expectedEnv { for k, v := range expectedEnv {
if e, a := v, varMap[k]; e.edited != a || e.count != varCounts[k] { if e, a := v, declaredEnv[k]; e.edited != a || e.count != counts[k] {
t.Errorf("Expected %v count=%d, got %v count=%d", t.Errorf("Expected %v count=%d, got %v count=%d",
e.edited, e.count, a, varCounts[k]) e.edited, e.count, a, counts[k])
} else { } else {
delete(varMap, k) delete(declaredEnv, k)
} }
} }
if len(varMap) != 0 { if len(declaredEnv) != 0 {
t.Errorf("Unexpected keys in declared env: %v", varMap) t.Errorf("Unexpected keys in declared env: %v", declaredEnv)
} }
} }
func TestMapping(t *testing.T) { func TestMapping(t *testing.T) {
context := map[string]interface{}{
"VAR_A": "A",
"VAR_B": "B",
"VAR_C": "C",
"VAR_REF": "$(VAR_A)",
"VAR_EMPTY": "",
}
doExpansionTest(t, context)
}
func TestMappingDual(t *testing.T) {
context := map[string]interface{}{
"VAR_A": "A",
"VAR_EMPTY": "",
}
context2 := map[string]interface{}{
"VAR_B": "B",
"VAR_C": "C",
"VAR_REF": "$(VAR_A)",
}
doExpansionTest(t, context, context2)
}
func doExpansionTest(t *testing.T, context ...map[string]interface{}) {
cases := []struct { cases := []struct {
name string name string
input string input string
@@ -351,17 +333,11 @@ func TestMapping(t *testing.T) {
expected: "\n", expected: "\n",
}, },
} }
for _, tc := range cases { for _, tc := range cases {
counts := make(map[string]int) counts := make(map[string]int)
expanded := DoReplacements( mapping := MappingFuncFor(counts, context...)
fmt.Sprintf("%v", tc.input), expanded := Expand(fmt.Sprintf("%v", tc.input), mapping)
MakePrimitiveReplacer(counts, map[string]interface{}{
"VAR_A": "A",
"VAR_B": "B",
"VAR_C": "C",
"VAR_REF": "$(VAR_A)",
"VAR_EMPTY": "",
}))
if e, a := tc.expected, expanded; e != a { if e, a := tc.expected, expanded; e != a {
t.Errorf("%v: expected %q, got %q", tc.name, e, a) t.Errorf("%v: expected %q, got %q", tc.name, e, a)
} }
@@ -371,7 +347,8 @@ func TestMapping(t *testing.T) {
} }
if len(tc.counts) > 0 { if len(tc.counts) > 0 {
for k, expectedCount := range tc.counts { for k, expectedCount := range tc.counts {
if c, ok := counts[k]; ok { c, ok := counts[k]
if ok {
if c != expectedCount { if c != expectedCount {
t.Errorf( t.Errorf(
"%v: k=%s, expected count %d, got %d", "%v: k=%s, expected count %d, got %d",

View File

@@ -7,26 +7,19 @@ import (
"encoding/json" "encoding/json"
"strings" "strings"
"github.com/go-openapi/spec"
"github.com/pkg/errors" "github.com/pkg/errors"
"k8s.io/kube-openapi/pkg/validation/spec" "k8s.io/kube-openapi/pkg/common"
"sigs.k8s.io/kustomize/api/filesys"
"sigs.k8s.io/kustomize/api/ifc" "sigs.k8s.io/kustomize/api/ifc"
"sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig" "sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig"
"sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/filesys"
"sigs.k8s.io/kustomize/kyaml/resid"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
// OpenAPIDefinition describes single type. type myProperties map[string]spec.Schema
// Normally these definitions are auto-generated using gen-openapi. type nameToApiMap map[string]common.OpenAPIDefinition
// Same as in k8s.io / kube-openapi / pkg / common.
type OpenAPIDefinition struct {
Schema spec.Schema
Dependencies []string
}
type myProperties = map[string]spec.Schema
type nameToApiMap map[string]OpenAPIDefinition
// LoadConfigFromCRDs parse CRD schemas from paths into a TransformerConfig // LoadConfigFromCRDs parse CRD schemas from paths into a TransformerConfig
func LoadConfigFromCRDs( func LoadConfigFromCRDs(
@@ -169,7 +162,7 @@ func loadCrdIntoConfig(
err = theConfig.AddNamereferenceFieldSpec( err = theConfig.AddNamereferenceFieldSpec(
builtinconfig.NameBackReferences{ builtinconfig.NameBackReferences{
Gvk: resid.Gvk{Kind: kind, Version: version}, Gvk: resid.Gvk{Kind: kind, Version: version},
Referrers: []types.FieldSpec{ FieldSpecs: []types.FieldSpec{
makeFs(theGvk, append(path, propName, nameKey))}, makeFs(theGvk, append(path, propName, nameKey))},
}) })
if err != nil { if err != nil {
@@ -178,12 +171,9 @@ func loadCrdIntoConfig(
} }
} }
if property.Ref.GetURL() != nil { if property.Ref.GetURL() != nil {
err = loadCrdIntoConfig( loadCrdIntoConfig(
theConfig, theGvk, theMap, theConfig, theGvk, theMap,
property.Ref.String(), append(path, propName)) property.Ref.String(), append(path, propName))
if err != nil {
return
}
} }
} }
return nil return nil

View File

@@ -7,13 +7,13 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/stretchr/testify/require" "sigs.k8s.io/kustomize/api/filesys"
"sigs.k8s.io/kustomize/api/loader"
. "sigs.k8s.io/kustomize/api/internal/accumulator" . "sigs.k8s.io/kustomize/api/internal/accumulator"
"sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig" "sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig"
"sigs.k8s.io/kustomize/api/loader" "sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/filesys"
"sigs.k8s.io/kustomize/kyaml/resid"
) )
// This defines two CRD's: Bee and MyKind. // This defines two CRD's: Bee and MyKind.
@@ -140,19 +140,21 @@ func TestLoadCRDs(t *testing.T) {
nbrs := []builtinconfig.NameBackReferences{ nbrs := []builtinconfig.NameBackReferences{
{ {
Gvk: resid.Gvk{Kind: "Secret", Version: "v1"}, Gvk: resid.Gvk{Kind: "Secret", Version: "v1"},
Referrers: []types.FieldSpec{ FieldSpecs: []types.FieldSpec{
{ {
Gvk: resid.Gvk{Kind: "MyKind"}, CreateIfNotPresent: false,
Path: "spec/secretRef/name", Gvk: resid.Gvk{Kind: "MyKind"},
Path: "spec/secretRef/name",
}, },
}, },
}, },
{ {
Gvk: resid.Gvk{Kind: "Bee", Version: "v1beta1"}, Gvk: resid.Gvk{Kind: "Bee", Version: "v1beta1"},
Referrers: []types.FieldSpec{ FieldSpecs: []types.FieldSpec{
{ {
Gvk: resid.Gvk{Kind: "MyKind"}, CreateIfNotPresent: false,
Path: "spec/beeRef/name", Gvk: resid.Gvk{Kind: "MyKind"},
Path: "spec/beeRef/name",
}, },
}, },
}, },
@@ -163,13 +165,16 @@ func TestLoadCRDs(t *testing.T) {
} }
fSys := filesys.MakeFsInMemory() fSys := filesys.MakeFsInMemory()
err := fSys.WriteFile("/testpath/crd.json", []byte(crdContent)) fSys.WriteFile("/testpath/crd.json", []byte(crdContent))
require.NoError(t, err)
ldr, err := loader.NewLoader(loader.RestrictionRootOnly, "/testpath", fSys) ldr, err := loader.NewLoader(loader.RestrictionRootOnly, "/testpath", fSys)
require.NoError(t, err) if err != nil {
t.Fatalf("unexpected error:%v", err)
}
actualTc, err := LoadConfigFromCRDs(ldr, []string{"crd.json"}) actualTc, err := LoadConfigFromCRDs(ldr, []string{"crd.json"})
require.NoError(t, err) if err != nil {
t.Fatalf("unexpected error:%v", err)
}
if !reflect.DeepEqual(actualTc, expectedTc) { if !reflect.DeepEqual(actualTc, expectedTc) {
t.Fatalf("expected\n %v\n but got\n %v\n", expectedTc, actualTc) t.Fatalf("expected\n %v\n but got\n %v\n", expectedTc, actualTc)
} }

View File

@@ -4,30 +4,23 @@
package accumulator package accumulator
import ( import (
"fmt"
"log" "log"
"sigs.k8s.io/kustomize/api/filters/nameref" "sigs.k8s.io/kustomize/api/filters/nameref"
"sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig" "sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig"
"sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/resource" "sigs.k8s.io/kustomize/kyaml/filtersutil"
"sigs.k8s.io/kustomize/kyaml/resid"
) )
type nameReferenceTransformer struct { type nameReferenceTransformer struct {
backRefs []builtinconfig.NameBackReferences backRefs []builtinconfig.NameBackReferences
} }
const doDebug = false
var _ resmap.Transformer = &nameReferenceTransformer{} var _ resmap.Transformer = &nameReferenceTransformer{}
type filterMap map[*resource.Resource][]nameref.Filter
// newNameReferenceTransformer constructs a nameReferenceTransformer // newNameReferenceTransformer constructs a nameReferenceTransformer
// with a given slice of NameBackReferences. // with a given slice of NameBackReferences.
func newNameReferenceTransformer( func newNameReferenceTransformer(br []builtinconfig.NameBackReferences) resmap.Transformer {
br []builtinconfig.NameBackReferences) resmap.Transformer {
if br == nil { if br == nil {
log.Fatal("backrefs not expected to be nil") log.Fatal("backrefs not expected to be nil")
} }
@@ -40,64 +33,13 @@ func newNameReferenceTransformer(
// //
// For example, a HorizontalPodAutoscaler (HPA) // For example, a HorizontalPodAutoscaler (HPA)
// necessarily refers to a Deployment, the thing that // necessarily refers to a Deployment, the thing that
// an HPA scales. In this case: // the HPA scales. The Deployment name might change
// (e.g. prefix added), and the reference in the HPA
// has to be fixed.
// //
// - the HPA instance is the Referrer, // In the outer loop over the ResMap below, say we
// - the Deployment instance is the ReferralTarget. // encounter a specific HPA. Then, in scanning backrefs,
// // we encounter an entry like
// If the Deployment's name changes, e.g. a prefix is added,
// then the HPA's reference to the Deployment must be fixed.
//
func (t *nameReferenceTransformer) Transform(m resmap.ResMap) error {
fMap := t.determineFilters(m.Resources())
debug(fMap)
for r, fList := range fMap {
c, err := m.SubsetThatCouldBeReferencedByResource(r)
if err != nil {
return err
}
for _, f := range fList {
f.Referrer = r
f.ReferralCandidates = c
if err := f.Referrer.ApplyFilter(f); err != nil {
return err
}
}
}
return nil
}
func debug(fMap filterMap) {
if !doDebug {
return
}
fmt.Printf("filterMap has %d entries:\n", len(fMap))
rCount := 0
for r, fList := range fMap {
yml, _ := r.AsYAML()
rCount++
fmt.Printf(`
---- %3d. possible referrer -------------
%s
---------`, rCount, string(yml),
)
for i, f := range fList {
fmt.Printf(`
%3d/%3d update: %s
from: %s
`, rCount, i+1, f.NameFieldToUpdate.Path, f.ReferralTarget,
)
}
}
}
// Produce a map from referrer resources that might need to be fixed
// to filters that might fix them. The keys to this map are potential
// referrers, so won't include resources like ConfigMap or Secret.
//
// In the inner loop over the resources below, say we
// encounter an HPA instance. Then, in scanning the set
// of all known backrefs, we encounter an entry like
// //
// - kind: Deployment // - kind: Deployment
// fieldSpecs: // fieldSpecs:
@@ -106,60 +48,54 @@ func debug(fMap filterMap) {
// //
// This entry says that an HPA, via its // This entry says that an HPA, via its
// 'spec/scaleTargetRef/name' field, may refer to a // 'spec/scaleTargetRef/name' field, may refer to a
// Deployment. // Deployment. This match to HPA means we may need to
// modify the value in its 'spec/scaleTargetRef/name'
// field, by searching for the thing it refers to,
// and getting its new name.
// //
// This means that a filter will need to hunt for the right Deployment, // As a filter, and search optimization, we compute a
// obtain it's new name, and write that name into the HPA's // subset of all resources that the HPA could refer to,
// 'spec/scaleTargetRef/name' field. Return a filter that can do that. // by excluding objects from other namespaces, and
func (t *nameReferenceTransformer) determineFilters( // excluding objects that don't have the same prefix-
resources []*resource.Resource) (fMap filterMap) { // suffix mods as the HPA.
//
// We cache the resource OrgId values because they don't change and otherwise are very visible in a memory pprof // We look in this subset for all Deployment objects
resourceOrgIds := make([]resid.ResId, len(resources)) // with a resId that has a Name matching the field value
for i, resource := range resources { // present in the HPA. If no match do nothing; if more
resourceOrgIds[i] = resource.OrgId() // than one match, it's an error.
} //
// We overwrite the HPA name field with the value found
fMap = make(filterMap) // in the Deployment's name field (the name in the raw
for _, backReference := range t.backRefs { // object - the modified name - not the unmodified name
for _, referrerSpec := range backReference.Referrers { // in the Deployment's resId).
for i, res := range resources { //
if resourceOrgIds[i].IsSelected(&referrerSpec.Gvk) { // This process assumes that the name stored in a ResId
// If this is true, the res might be a referrer, and if // (the ResMap key) isn't modified by name transformers.
// so, the name reference it holds might need an update. // Name transformers should only modify the name in the
if resHasField(res, referrerSpec.Path) { // body of the resource object (the value in the ResMap).
// Optimization - the referrer has the field //
// that might need updating. func (o *nameReferenceTransformer) Transform(m resmap.ResMap) error {
fMap[res] = append(fMap[res], nameref.Filter{ // TODO: Too much looping, here and in transitive calls.
// Name field to write in the Referrer. for _, referrer := range m.Resources() {
// If the path specified here isn't found in var candidates resmap.ResMap
// the Referrer, nothing happens (no error, for _, target := range o.backRefs {
// no field creation). for _, fSpec := range target.FieldSpecs {
NameFieldToUpdate: referrerSpec, if referrer.OrgId().IsSelected(&fSpec.Gvk) {
// Specification of object class to read from. if candidates == nil {
// Always read from metadata/name field. candidates = m.SubsetThatCouldBeReferencedByResource(referrer)
ReferralTarget: backReference.Gvk, }
}) err := filtersutil.ApplyToJSON(nameref.Filter{
FieldSpec: fSpec,
Referrer: referrer,
Target: target.Gvk,
ReferralCandidates: candidates,
}, referrer)
if err != nil {
return err
} }
} }
} }
} }
} }
return fMap return nil
}
// TODO: check res for field existence here to avoid extra work.
// res.GetFieldValue, which uses yaml.Lookup under the hood, doesn't know
// how to parse fieldspec-style paths that make no distinction
// between maps and sequences. This means it cannot lookup commonly
// used "indeterminate" paths like
// spec/containers/env/valueFrom/configMapKeyRef/name
// ('containers' is a list, not a map).
// However, the fieldspec filter does know how to handle this;
// extract that code and call it here?
func resHasField(res *resource.Resource, path string) bool {
return true
// fld := strings.Join(utils.PathSplitter(path), ".")
// _, e := res.GetFieldValue(fld)
// return e == nil
} }

View File

@@ -8,16 +8,17 @@ import (
"testing" "testing"
"sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig" "sigs.k8s.io/kustomize/api/internal/plugins/builtinconfig"
"sigs.k8s.io/kustomize/api/provider" "sigs.k8s.io/kustomize/api/k8sdeps/kunstruct"
"sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/resource"
resmaptest_test "sigs.k8s.io/kustomize/api/testutils/resmaptest" resmaptest_test "sigs.k8s.io/kustomize/api/testutils/resmaptest"
"sigs.k8s.io/kustomize/kyaml/resid"
) )
const notEqualErrFmt = "expected (self) doesn't match actual (other): %v"
func TestNameReferenceHappyRun(t *testing.T) { func TestNameReferenceHappyRun(t *testing.T) {
m := resmaptest_test.NewRmBuilderDefault(t).AddWithName( rf := resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl())
m := resmaptest_test.NewRmBuilder(t, rf).AddWithName(
"cm1", "cm1",
map[string]interface{}{ map[string]interface{}{
"apiVersion": "v1", "apiVersion": "v1",
@@ -219,7 +220,6 @@ func TestNameReferenceHappyRun(t *testing.T) {
"secret1", "secret1",
"secret1", "secret1",
"secret2", "secret2",
"cm1",
}, },
}, },
}, },
@@ -261,8 +261,7 @@ func TestNameReferenceHappyRun(t *testing.T) {
}, },
}).ResMap() }).ResMap()
expected := resmaptest_test.NewSeededRmBuilderDefault( expected := resmaptest_test.NewSeededRmBuilder(t, rf, m.ShallowCopy()).ReplaceResource(
t, m.ShallowCopy()).ReplaceResource(
map[string]interface{}{ map[string]interface{}{
"group": "apps", "group": "apps",
"apiVersion": "v1", "apiVersion": "v1",
@@ -423,7 +422,6 @@ func TestNameReferenceHappyRun(t *testing.T) {
"someprefix-secret1-somehash", "someprefix-secret1-somehash",
"someprefix-secret1-somehash", "someprefix-secret1-somehash",
"secret2", "secret2",
"someprefix-cm1-somehash",
}, },
}, },
}, },
@@ -472,17 +470,19 @@ func TestNameReferenceHappyRun(t *testing.T) {
} }
if err = expected.ErrorIfNotEqualLists(m); err != nil { if err = expected.ErrorIfNotEqualLists(m); err != nil {
t.Fatalf(notEqualErrFmt, err) t.Fatalf("actual doesn't match expected: %v", err)
} }
} }
func TestNameReferenceUnhappyRun(t *testing.T) { func TestNameReferenceUnhappyRun(t *testing.T) {
rf := resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl())
tests := []struct { tests := []struct {
resMap resmap.ResMap resMap resmap.ResMap
expectedErr string expectedErr string
}{ }{
{ {
resMap: resmaptest_test.NewRmBuilderDefault(t).Add( resMap: resmaptest_test.NewRmBuilder(t, rf).Add(
map[string]interface{}{ map[string]interface{}{
"apiVersion": "rbac.authorization.k8s.io/v1", "apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "ClusterRole", "kind": "ClusterRole",
@@ -502,7 +502,7 @@ func TestNameReferenceUnhappyRun(t *testing.T) {
}).ResMap(), }).ResMap(),
expectedErr: "is expected to be"}, expectedErr: "is expected to be"},
{ {
resMap: resmaptest_test.NewRmBuilderDefault(t).Add( resMap: resmaptest_test.NewRmBuilder(t, rf).Add(
map[string]interface{}{ map[string]interface{}{
"apiVersion": "rbac.authorization.k8s.io/v1", "apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "ClusterRole", "kind": "ClusterRole",
@@ -520,10 +520,7 @@ func TestNameReferenceUnhappyRun(t *testing.T) {
}, },
}, },
}).ResMap(), }).ResMap(),
expectedErr: `updating name reference in 'rules/resourceNames' field of 'ClusterRole.v1.rbac.authorization.k8s.io/cr.[noNs]': ` + expectedErr: "cannot find field 'name' in node"},
`considering field 'rules/resourceNames' of object ClusterRole.v1.rbac.authorization.k8s.io/cr.[noNs]: visit traversal on ` +
`path: [resourceNames]: path config error; no 'name' field in node`,
},
} }
nrt := newNameReferenceTransformer(builtinconfig.MakeDefaultConfig().NameReference) nrt := newNameReferenceTransformer(builtinconfig.MakeDefaultConfig().NameReference)
@@ -534,14 +531,15 @@ func TestNameReferenceUnhappyRun(t *testing.T) {
} }
if !strings.Contains(err.Error(), test.expectedErr) { if !strings.Contains(err.Error(), test.expectedErr) {
t.Fatalf("Incorrect error.\nExpected:\n %s\nGot:\n%v", t.Fatalf("Incorrect error.\nExpected: %s, but got %v",
test.expectedErr, err) test.expectedErr, err)
} }
} }
} }
func TestNameReferencePersistentVolumeHappyRun(t *testing.T) { func TestNameReferencePersistentVolumeHappyRun(t *testing.T) {
rf := provider.NewDefaultDepProvider().GetResourceFactory() rf := resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl())
v1 := rf.FromMapWithName( v1 := rf.FromMapWithName(
"volume1", "volume1",
@@ -592,7 +590,7 @@ func TestNameReferencePersistentVolumeHappyRun(t *testing.T) {
v2.AppendRefBy(c2.CurId()) v2.AppendRefBy(c2.CurId())
if err := m1.ErrorIfNotEqualLists(m2); err != nil { if err := m1.ErrorIfNotEqualLists(m2); err != nil {
t.Fatalf(notEqualErrFmt, err) t.Fatalf("actual doesn't match expected: %v", err)
} }
} }
@@ -666,7 +664,9 @@ const (
// object with the same original names (uniquename) in different namespaces // object with the same original names (uniquename) in different namespaces
// and with different current Id. // and with different current Id.
func TestNameReferenceNamespace(t *testing.T) { func TestNameReferenceNamespace(t *testing.T) {
m := resmaptest_test.NewRmBuilderDefault(t). rf := resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl())
m := resmaptest_test.NewRmBuilder(t, rf).
// Add ConfigMap with the same org name in noNs, "ns1" and "ns2" namespaces // Add ConfigMap with the same org name in noNs, "ns1" and "ns2" namespaces
AddWithName(orgname, map[string]interface{}{ AddWithName(orgname, map[string]interface{}{
"apiVersion": "v1", "apiVersion": "v1",
@@ -715,7 +715,7 @@ func TestNameReferenceNamespace(t *testing.T) {
AddWithNsAndName(ns1, orgname, deploymentMap(ns1, prefixedname, orgname, orgname)). AddWithNsAndName(ns1, orgname, deploymentMap(ns1, prefixedname, orgname, orgname)).
AddWithNsAndName(ns2, orgname, deploymentMap(ns2, suffixedname, orgname, orgname)).ResMap() AddWithNsAndName(ns2, orgname, deploymentMap(ns2, suffixedname, orgname, orgname)).ResMap()
expected := resmaptest_test.NewSeededRmBuilderDefault(t, m.ShallowCopy()). expected := resmaptest_test.NewSeededRmBuilder(t, rf, m.ShallowCopy()).
ReplaceResource(deploymentMap(defaultNs, modifiedname, modifiedname, modifiedname)). ReplaceResource(deploymentMap(defaultNs, modifiedname, modifiedname, modifiedname)).
ReplaceResource(deploymentMap(ns1, prefixedname, prefixedname, prefixedname)). ReplaceResource(deploymentMap(ns1, prefixedname, prefixedname, prefixedname)).
ReplaceResource(deploymentMap(ns2, suffixedname, suffixedname, suffixedname)).ResMap() ReplaceResource(deploymentMap(ns2, suffixedname, suffixedname, suffixedname)).ResMap()
@@ -726,9 +726,8 @@ func TestNameReferenceNamespace(t *testing.T) {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
m.RemoveBuildAnnotations()
if err = expected.ErrorIfNotEqualLists(m); err != nil { if err = expected.ErrorIfNotEqualLists(m); err != nil {
t.Fatalf(notEqualErrFmt, err) t.Fatalf("actual doesn't match expected: %v", err)
} }
} }
@@ -736,7 +735,9 @@ func TestNameReferenceNamespace(t *testing.T) {
// object with the same original names (uniquename) in different namespaces // object with the same original names (uniquename) in different namespaces
// and with different current Id. // and with different current Id.
func TestNameReferenceClusterWide(t *testing.T) { func TestNameReferenceClusterWide(t *testing.T) {
m := resmaptest_test.NewRmBuilderDefault(t). rf := resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl())
m := resmaptest_test.NewRmBuilder(t, rf).
// Add ServiceAccount with the same org name in noNs, "ns1" and "ns2" namespaces // Add ServiceAccount with the same org name in noNs, "ns1" and "ns2" namespaces
AddWithName(orgname, map[string]interface{}{ AddWithName(orgname, map[string]interface{}{
"apiVersion": "v1", "apiVersion": "v1",
@@ -788,9 +789,9 @@ func TestNameReferenceClusterWide(t *testing.T) {
"name": modifiedname, "name": modifiedname,
}, },
"roleRef": map[string]interface{}{ "roleRef": map[string]interface{}{
"apiGroup": "rbac.authorization.k8s.io", "apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "ClusterRole", "kind": "ClusterRole",
"name": orgname, "name": orgname,
}, },
"subjects": []interface{}{ "subjects": []interface{}{
map[string]interface{}{ map[string]interface{}{
@@ -815,7 +816,7 @@ func TestNameReferenceClusterWide(t *testing.T) {
}, },
}}).ResMap() }}).ResMap()
expected := resmaptest_test.NewSeededRmBuilderDefault(t, m.ShallowCopy()). expected := resmaptest_test.NewSeededRmBuilder(t, rf, m.ShallowCopy()).
ReplaceResource( ReplaceResource(
map[string]interface{}{ map[string]interface{}{
"apiVersion": "rbac.authorization.k8s.io/v1", "apiVersion": "rbac.authorization.k8s.io/v1",
@@ -844,9 +845,9 @@ func TestNameReferenceClusterWide(t *testing.T) {
"name": modifiedname, "name": modifiedname,
}, },
"roleRef": map[string]interface{}{ "roleRef": map[string]interface{}{
"apiGroup": "rbac.authorization.k8s.io", "apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "ClusterRole", "kind": "ClusterRole",
"name": modifiedname, "name": modifiedname,
}, },
// The following tests required a change in // The following tests required a change in
// getNameFunc implementation in order to leverage // getNameFunc implementation in order to leverage
@@ -876,9 +877,9 @@ func TestNameReferenceClusterWide(t *testing.T) {
}).ResMap() }).ResMap()
clusterRoleId := resid.NewResId( clusterRoleId := resid.NewResId(
resid.NewGvk("rbac.authorization.k8s.io", "v1", "ClusterRole"), modifiedname) resid.Gvk{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRole"}, modifiedname)
clusterRoleBindingId := resid.NewResId( clusterRoleBindingId := resid.NewResId(
resid.NewGvk("rbac.authorization.k8s.io", "v1", "ClusterRoleBinding"), modifiedname) resid.Gvk{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBinding"}, modifiedname)
clusterRole, _ := expected.GetByCurrentId(clusterRoleId) clusterRole, _ := expected.GetByCurrentId(clusterRoleId)
clusterRole.AppendRefBy(clusterRoleBindingId) clusterRole.AppendRefBy(clusterRoleBindingId)
@@ -888,11 +889,8 @@ func TestNameReferenceClusterWide(t *testing.T) {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
expected.RemoveBuildAnnotations()
m.RemoveBuildAnnotations()
if err = expected.ErrorIfNotEqualLists(m); err != nil { if err = expected.ErrorIfNotEqualLists(m); err != nil {
t.Fatalf(notEqualErrFmt, err) t.Fatalf("actual doesn't match expected: %v", err)
} }
} }
@@ -900,7 +898,9 @@ func TestNameReferenceClusterWide(t *testing.T) {
// object with the same original names (uniquename) in different namespaces // object with the same original names (uniquename) in different namespaces
// and with different current Id. // and with different current Id.
func TestNameReferenceNamespaceTransformation(t *testing.T) { func TestNameReferenceNamespaceTransformation(t *testing.T) {
m := resmaptest_test.NewRmBuilderDefault(t). rf := resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl())
m := resmaptest_test.NewRmBuilder(t, rf).
AddWithNsAndName(ns4, orgname, map[string]interface{}{ AddWithNsAndName(ns4, orgname, map[string]interface{}{
"apiVersion": "v1", "apiVersion": "v1",
"kind": "Secret", "kind": "Secret",
@@ -937,9 +937,9 @@ func TestNameReferenceNamespaceTransformation(t *testing.T) {
"name": modifiedname, "name": modifiedname,
}, },
"roleRef": map[string]interface{}{ "roleRef": map[string]interface{}{
"apiGroup": "rbac.authorization.k8s.io", "apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "ClusterRole", "kind": "ClusterRole",
"name": orgname, "name": orgname,
}, },
"subjects": []interface{}{ "subjects": []interface{}{
map[string]interface{}{ map[string]interface{}{
@@ -964,7 +964,7 @@ func TestNameReferenceNamespaceTransformation(t *testing.T) {
}, },
}}).ResMap() }}).ResMap()
expected := resmaptest_test.NewSeededRmBuilderDefault(t, m.ShallowCopy()). expected := resmaptest_test.NewSeededRmBuilder(t, rf, m.ShallowCopy()).
ReplaceResource( ReplaceResource(
map[string]interface{}{ map[string]interface{}{
"apiVersion": "rbac.authorization.k8s.io/v1", "apiVersion": "rbac.authorization.k8s.io/v1",
@@ -973,9 +973,9 @@ func TestNameReferenceNamespaceTransformation(t *testing.T) {
"name": modifiedname, "name": modifiedname,
}, },
"roleRef": map[string]interface{}{ "roleRef": map[string]interface{}{
"apiGroup": "rbac.authorization.k8s.io", "apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "ClusterRole", "kind": "ClusterRole",
"name": modifiedname, "name": modifiedname,
}, },
// The following tests required a change in // The following tests required a change in
// getNameFunc implementation in order to leverage // getNameFunc implementation in order to leverage
@@ -1005,11 +1005,9 @@ func TestNameReferenceNamespaceTransformation(t *testing.T) {
}).ResMap() }).ResMap()
clusterRoleId := resid.NewResId( clusterRoleId := resid.NewResId(
resid.NewGvk("rbac.authorization.k8s.io", "v1", "ClusterRole"), resid.Gvk{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRole"}, modifiedname)
modifiedname)
clusterRoleBindingId := resid.NewResId( clusterRoleBindingId := resid.NewResId(
resid.NewGvk("rbac.authorization.k8s.io", "v1", "ClusterRoleBinding"), resid.Gvk{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBinding"}, modifiedname)
modifiedname)
clusterRole, _ := expected.GetByCurrentId(clusterRoleId) clusterRole, _ := expected.GetByCurrentId(clusterRoleId)
clusterRole.AppendRefBy(clusterRoleBindingId) clusterRole.AppendRefBy(clusterRoleBindingId)
@@ -1019,9 +1017,8 @@ func TestNameReferenceNamespaceTransformation(t *testing.T) {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
m.RemoveBuildAnnotations()
if err = expected.ErrorIfNotEqualLists(m); err != nil { if err = expected.ErrorIfNotEqualLists(m); err != nil {
t.Fatalf(notEqualErrFmt, err) t.Fatalf("actual doesn't match expected: %v", err)
} }
} }
@@ -1029,7 +1026,9 @@ func TestNameReferenceNamespaceTransformation(t *testing.T) {
// It validates the change done is IsSameFuzzyNamespace which // It validates the change done is IsSameFuzzyNamespace which
// uses the IsNsEquals method instead of the simple == operator. // uses the IsNsEquals method instead of the simple == operator.
func TestNameReferenceCandidateSelection(t *testing.T) { func TestNameReferenceCandidateSelection(t *testing.T) {
m := resmaptest_test.NewRmBuilderDefault(t). rf := resource.NewFactory(
kunstruct.NewKunstructuredFactoryImpl())
m := resmaptest_test.NewRmBuilder(t, rf).
AddWithName("cm1", map[string]interface{}{ AddWithName("cm1", map[string]interface{}{
"apiVersion": "v1", "apiVersion": "v1",
"kind": "ConfigMap", "kind": "ConfigMap",
@@ -1046,7 +1045,7 @@ func TestNameReferenceCandidateSelection(t *testing.T) {
AddWithName("deploy1", deploymentMap("", "p1-deploy1", "cm1", "secret1")). AddWithName("deploy1", deploymentMap("", "p1-deploy1", "cm1", "secret1")).
ResMap() ResMap()
expected := resmaptest_test.NewSeededRmBuilderDefault(t, m.ShallowCopy()). expected := resmaptest_test.NewSeededRmBuilder(t, rf, m.ShallowCopy()).
ReplaceResource(deploymentMap("", "p1-deploy1", "p1-cm1-hash", "p1-secret1-hash")). ReplaceResource(deploymentMap("", "p1-deploy1", "p1-cm1-hash", "p1-secret1-hash")).
ResMap() ResMap()
@@ -1056,8 +1055,7 @@ func TestNameReferenceCandidateSelection(t *testing.T) {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
m.RemoveBuildAnnotations()
if err = expected.ErrorIfNotEqualLists(m); err != nil { if err = expected.ErrorIfNotEqualLists(m); err != nil {
t.Fatalf(notEqualErrFmt, err) t.Fatalf("actual doesn't match expected: %v", err)
} }
} }

Some files were not shown because too many files have changed in this diff Show More