Description
Eschewed features
- This issue is not requesting templating, unstuctured edits, build-time side-effects from args or env vars, or any other eschewed feature.
What would you like to have added?
Add an ignoreMissing
flag in the ReplacementTransformer's options field.
It would allow users to opt for the pre 5.0.0 / #4789 behavior where missing fields in a target resource were ignored instead of resulting in errors.
Why is this needed?
This feature is needed to address a significant change in Kustomize 5.0.0 where replacements now fail if a targeted field is missing from a resource and options.create
isn't set.
The previous behavior was to ignore invalid targets, which allowed users to package commonly used replacements (such as GCP Project ID replacements) as widely reusable components, that could be imported from various service directories with similar-but-not-quite-identical resources, modifying only relevant fields and skipping missing ones without errors. In the absence of parameterized components, this allowed for much needed flexibility in broadly applying replacements to 90% similar resources.
The new behavior, where replacements fail if a target field is missing, significantly disrupts workflows that previously depended on silent skipping of non-matching replacements. This is especially problematic in scenarios like ours, where environment-specific GCP project IDs appear in various formats across Kubernetes manifests, necessitating a universal replacement approach.
Detailed Use Case
In our Kustomize codebase, we deal with environment-specific GCP project IDs in otherwise identical resources. These IDs can appear in multiple formats - as a standalone string, as projects/GCP_PROJECT_ID
, or as part of a service account ID (service-account@GCP_PROJECT_ID.iam.gserviceaccount.com
), etc. A shared component performing generic replacements of these IDs in our Config Connector resource types is crucial for reducing repetition in our repository.
Here is an example generic replacement I intended to use, which lists all possible targets for GCP_PROJECT_ID
. From 5.0.0 it will throw an error if any of the fields in the selected targets are missing.
# _components/replacements/common/gcp_project_id.yaml
# This replacement fills in the GCP project ID (e.g. development-1234567) in places where it can be easily delimited.
source:
kind: ConfigMap
name: replacements
fieldPath: data.GCP_PROJECT_ID
targets:
# IAMPolicyMember external resourceRefs (projects/PROJECT_ID)
- select:
kind: IAMPolicyMember
fieldPaths:
- spec.resourceRef.external
options:
delimiter: "/"
index: 1
# PubSubSchema external projectRef (PROJECT_ID)
- select:
kind: PubSubSchema
fieldPaths:
- spec.projectRef.external
Here's another generic replacement, covering a different format for the GCP project ID (GCP_PROJECT_ID.iam.gserviceaccount.com
)
(As a side note : breaking this down into several replacements is only necessary because options.delimiter
is quite limited and doesn't support regexes. And of course, this would be a trivial task with unstructured edits ;) )
# _components/replacements/common/gcp_sa_domain.yaml
# This replacement fills in the GCP service account domain (e.g. development-1234567.iam.gserviceaccount.com).
# In most cases it only replaces the part after the @ sign with the GCP_SA_DOMAIN value from the ConfigMap, and keeps the service account name intact.
# The GCP_PROJECT_ID replacement is too generic for this purpose, because there can be only one delimiter and index per replacement target.
source:
kind: ConfigMap
name: replacements
fieldPath: data.GCP_SA_DOMAIN
targets:
# iam.gke.io/gcp-service-account annotations (service-account@domain)
- select:
kind: ServiceAccount
fieldPaths:
- metadata.annotations.[iam.gke.io/gcp-service-account]
options:
delimiter: "@"
index: 1
# IAMPolicyMember member field (service-account@domain)
- select:
kind: IAMPolicyMember
fieldPaths:
- spec.member
options:
delimiter: "@"
index: 1
These generic replacement "recipes" are part of a common
component, used by our service definitions through environment-specific wrapper components that add a Configmap with relevant source values for this environment, as referenced in the replacements :
# _components/replacements/development/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
configMapGenerator:
- name: replacements
literals:
- "GCP_PROJECT_ID=development-1234567"
- "GCP_SA_DOMAIN=development-1234567.iam.gserviceaccount.com"
components:
- ../common
# Clean up the ConfigMap after applying replacements
patches:
- patch: |-
$patch: delete
apiVersion: v1
kind: ConfigMap
metadata:
name: replacements
This environment-specific component is then imported in the corresponding service overlays, like this :
# services/myservice/overlays/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
components:
- ../../../_components/replacements/development
# [More definitions here]
Can you accomplish the motivating task without this feature, and if so, how?
Only very inelegantly or with much repetition, AFAICT :
-
I could keep the generic replacements component idea but target the resources more precisely using
select
/reject
(realistically, it would have to be done by name. But regex support is broken by the same change, making that even harder). Also, adding specifics about the callers' resources in a component is an ugly case of leaky abstraction. -
I could stick to a generic replacement component that only applies the absolute lowest common denominator list of replacements that will work with each service that uses this component. But that means more service-specific replacements to be implemented, with a repetition of the source configMap (GCP Project ID in its different string incarnations) in the service overlays themselves, which is what we were trying to get away from in the first place. Also, the surface of lowest common denominators will shrink dramatically with each service we add, requiring the move of replacements previously used by other services in the services themselves. Sounds hellish.
-
I could give up on the idea of a generic "replacements" component, and apply replacements in each service. 90% of them would be repeated.
What other solutions have you considered?
-
Sticking to Kustomize <5.0, but our codebase requires this 5.1 behavior anyway.
-
Maintaining our own version of Kustomize with an
ignoreMissing
flag added in. -
Pushing for the introduction of parameterized components, or some other similar sort of flexibility-enhancing Kustomize feature.
-
Living with duplication in our code base, as a result of not being able to implement flexible enough reusable Kustomize components.
-
Giving up on a 100% Kustomize-native solution and introducing something like
envsubst
as a pre-processing step. -
Moving to Helm.
We haven't made a firm decision yet, but none of these options are appealing and I'd really like to give native Kustomize features a chance before we give up.
Anything else we should know?
My comment on #4789 explains the above perhaps more succinctly (apologies for the slightly passive-aggressive tone : written as I had just discovered this change which makes our solution fall apart.)
Both the consequences of this change and a flag that allows restoring the previous behavior have been discussed by several other users already in the comments of #4789 and #5128
Feature ownership
- I am interested in contributing this feature myself! 🎉