Skip to content

kaweezle/krmfnbuiltin

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

61 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

krmfnbuiltin

stability-beta

krmfnbuiltin is a kustomize plugin providing a set of KRM Functions that you can use to perform in place transformation in your kustomize projects.

Table of Contents
  1. Rationale
  2. Usage Example
  3. Use of generators
  4. Keeping or deleting generated resources
  5. Extensions
  6. Installation
  7. Argo CD integration
  8. Related projects

Rationale

kustomize fn run allows performing in place transformation of KRM (Kubernetes Resource Model) resources. This is handy to perform structured modification operations on GitOps repositories (aka shift left, see the functions tutorial and the KRM Functions Specification). Unfortunately, the builtin transformers are not available to kustomize fn run, as it expects the function to be contained in an external container or executable .

krmfnbuiltin provides both the image and executable allowing the use of any kustomize builtin transformer or generator, along with some additional goodies.

Usage Example

Let's imagine that you have a GitOps repository containing 10 Argo CD applications in the applications folder. The following is the manifest for one of them:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: argo-cd
  namespace: argocd
  annotations:
    autocloud/local-application: "true"
spec:
  destination:
    namespace: argocd
    server: https://kubernetes.default.svc
  ignoreDifferences:
    - group: argoproj.io
      jsonPointers:
        - /status
      kind: Application
  project: default
  source:
    repoURL: https://github.com/kaweezle/autocloud.git
    targetRevision: main
    path: packages/argocd
  syncPolicy:
    automated:
      allowEmpty: true
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

You have 9 other application manifests sharing the same snippet:

source:
  repoURL: https://github.com/kaweezle/autocloud.git
  targetRevision: main

Let's imagine now that you want to fork this repository for developing on another cluster. You obtain a new repository, https://github.com/myname/autocloud.git, on which you create a branch named feature/experiment for development. For the deployment to the development cluster to use the right repository and branch, you need to change repoURL and targetRevision for all the applications. You can do that by hand, but this is cumbersome and error prone.

On a Kustomization, you would have done:

patches:
    - patch: |-
        - op: replace
            path: /spec/source/repoURL
            value: https://github.com/myname/autocloud.git
        - op: replace
            path: /spec/source/targetRevision
            value: feature/experiment
    target:
        group: argoproj.io
        version: v1alpha1
        kind: Application
        # This annotation allow us to identify applications pointing locally
        annotationSelector: "autocloud/local-application=true"

But here you don't want to add a new kustomization nesting level. You just want to modify the actual application manifests on your branch. This is where KRM functions shine. To do that, you can write a function file in a functions directory:

# functions/fn-change-repo-and-branch.yaml
apiVersion: builtin
kind: PatchTransformer
metadata:
  name: fn-change-repo-and-branch
  annotations:
    # This will remove the internal annotations the transformer adds.
    config.kaweezle.com/cleanup: "true"
    config.kubernetes.io/function: |
      exec:
        path: krmfnbuiltin
    # Can also be:
    #  container:
    #    image: ghcr.io/kaweezle/krmfnbuiltin:v0.4.1
patch: |-
  - op: replace
      path: /spec/source/repoURL
      value: https://github.com/myname/autocloud.git
  - op: replace
      path: /spec/source/targetRevision
      value: feature/experiment
target:
  group: argoproj.io
  version: v1alpha1
  kind: Application
  # This annotation allow us to identify applications pointing locally
  annotationSelector: "autocloud/local-application=true"

And then you can apply your modification with the following command:

> kustomize fn run --enable-exec --fn-path functions applications

NOTE: the --enable-exec parameter is not needed if you use the container.

You obtain the desired modifications in the manifests:

source:
  repoURL: https://github.com/mysuer/autocloud.git
  targetRevision: feature/experiment

You now can commit the 10 modified manifests in your branch and deploy the applications.

Internal annotations cleanup

Some kustomize transformers add annotations to enable for instance reference reconciliation at the end of the build. These annotations have the prefix internal.config.kubernetes.io. When performing a kustomize build, kustomize removes them at the end of the process. When we are using the transformations in the context of a KRM function with kustomize fn run, the build cleanup is not performed. In consequence, the annotations would be added to the resource file touched by the transformation. For instance:

--- original.argocd.yaml
+++ transformed.argocd.yaml
@@ -5,6 +5,9 @@
   namespace: argocd
   annotations:
     autocloud/local: "true"
+    internal.config.kubernetes.io/previousKinds: Application
+    internal.config.kubernetes.io/previousNames: argo-cd
+    internal.config.kubernetes.io/previousNamespaces: argocd
 spec:
   destination:
     namespace: argocd

To avoid that, you can insert the following annotation:

config.kaweezle.com/cleanup: "true"

It will inform krmfnbuiltin that you are not using the transformer in the context of a bulid and that the internal annotations need to be removed.

Use of generators

krmfnbuiltin provides all the Kustomize builtin generators.

Let's imagine that one or more of your applications use an Helm chart that in turn creates applications. You pass the repo URL and target branch as values to the Helm Chart with the following:

helm:
  parameters:
    - name: common.targetRevision
      value: main
    - name: common.repoURL
      value: https://github.com/kaweezle/autocloud.git

For that particular transformation, JSON patches are not practical:

patch: |-
  - op: replace
    path: /spec/source/repoURL
    value: https://github.com/antoinemartin/autocloud.git
  - op: replace
    path: /spec/source/targetRevision
    value: deploy/citest
  - op: replace
    path: /spec/source/helm/parameters/1/value
    value: https://github.com/antoinemartin/autocloud.git
  - op: replace
    path: /spec/source/helm/parameters/0/value
    value: deploy/citest

You need to hardcode the index of the value to replace in the array, which is error prone, and you start duplicating values.

It would be better to have a unique source with the right values, and do replacements where needed. You can inject the values with a ConfigMapGenerator

# 01_configmap-generator.yaml
apiVersion: builtin
# Use this to inject current git values
# kind: GitConfigMapGenerator
kind: ConfigMapGenerator
metadata:
  name: configuration-map
  annotations:
    # This annotation will be transferred to the generated ConfigMap
    config.kaweezle.com/local-config: "true"
    config.kubernetes.io/function: |
      exec:
        path: krmfnbuiltin
# When using GitConfigMapGenerator, these are automatically injected
literals:
  - repoURL=https://github.com/kaweezle/autocloud.git
  - targetRevision=deploy/citest

And then use a ReplacementTransformer to inject the values:

# 02_replacement-transformer.yaml
apiVersion: builtin
kind: ReplacementTransformer
metadata:
  name: replacement-transformer
  namespace: argocd
  annotations:
    # Put this annotation in the last transformation to remove the generated resource
    config.kaweezle.com/prune-local: "true"
    config.kubernetes.io/function: |
      exec:
        path: krmfnbuiltin
replacements:
  - source:
      kind: ConfigMap
      fieldPath: data.repoURL
    targets:
      - select:
          kind: Application
          annotationSelector: "autocloud/local=true"
        fieldPaths:
          - spec.source.repoURL
          # This field specification is not related to the index
          - spec.source.helm.parameters.[name=common.repoURL].value
  - source:
      kind: ConfigMap
      fieldPath: data.targetRevision
    targets:
      - select:
          kind: Application
          annotationSelector: "autocloud/local=true"
        fieldPaths:
          - spec.source.targetRevision
          - spec.source.helm.parameters.[name=common.targetRevision].value

Some remarks:

  • ✔️ The actual values (repo url and revision) are only specified once in the config map generator.

  • ✔️ spec.source.helm.parameters.[name=common.repoURL].value is a path more specific than /spec/source/helm/parameters/1/value. The transformation would survive reordering.

  • ✔️ The functions file names are prefixed with a number prefix (01_, 02_) in order to ensure that the functions are executed in the right order. Note that you can group the two functions in one file separated by --- (this would make it unusable from kpt though).

  • ✔️ The generators contains the annotation:

    config.kaweezle.com/local-config: "true"

    that is injected in the generated resource.

  • ✔️ In the last transformation, we add the following annotation:

    config.kaweezle.com/prune-local: "true"

    In order to avoid saving the generated resources. In the presence of this annotation, krmfnbuiltin will remove all the resource having the config.kaweezle.com/local-config annotation.

Keeping or deleting generated resources

As said above, generated resources are saved by default. To prevent that, adding:

config.kaweezle.com/local-config: "true"

on the generators and:

config.kaweezle.com/prune-local: "true"

On the last transformation will remove those resources. In the absence of these annotations, the generated resources will be saved in a file named .krmfnbuiltin.yaml located in the configuration directory. You may want to add this file name to your .gitignore file in order to avoid committing it.

In some cases however, we want to inject new resources in the configuration. This can be done by just omitting the config.kaweezle.com/local-config annotation.

The name of the file containing the generated resources can be set with the following annotations:

  • config.kaweezle.com/path for the filename. If it contains directories, they will be created.
  • config.kaweezle.com/index For the starting index of the resources in the file.

Example:

apiVersion: builtin
kind: ConfigMapGenerator
metadata:
  name: configuration-map
  annotations:
    # config.kaweezle.com/local-config: "true"
    config.kaweezle.com/path: local-config.yaml
    config.kubernetes.io/function: |
      exec:
        path: krmfnbuiltin

With these annotations, the generated config map will be saved in the local-config.yaml file in the configuration directory.

If the file name is empty, i.e. the annotation is:

config.kaweezle.com/path: ""

The generated resources will be saved each in its own file with the pattern:

<namespace>/<kind>_<name>.yaml

For instance:

kube-flannel/daemonset_kube-flannel-ds.yaml

Extensions

This section describes the krmfnbuiltin additions to the Kustomize transformers and generators as well as the enhancements that have been made to some of them.

Remove Transformer

In the case the transformation(s) involves other transformers than krmfnbuiltin, the config.kaweezle.com/prune-local may not be available to remove resources injected in the transformation pipeline. For this use case, krmfnbuiltin provides RemoveTransformer:

apiVersion: builtin
kind: RemoveTransformer
metadata:
  name: replacement-transformer
  annotations:
    config.kubernetes.io/function: |
      exec:
        path: ../../krmfnbuiltin
targets:
  - annotationSelector: config.kaweezle.com/local-config

Each target specified in the targets field follows the patches target convention.

Note that you can use the Kustomize recommended method with a PatchStrategicMergeTransformer and a $patch: delete field. The above transformation is however more explicit.

ConfigMap generator with git properties

GitConfigMapGenerator work identically to ConfigMapGenerator except it adds two properties of the current git repository to the generated config map:

  • repoURL contains the URL or the remote specified by remoteName. by default, it takes the URL of the remote named origin.
  • targetRevision contains the name of the current branch.

This generator is useful in transformations that use those values, like for instance Argo CD application customization. Information about the configuration of the generator can be found in the ConfigMapGenerator kustomize documentation.

The following function configuration:

# 01_configmap-generator.yaml
apiVersion: builtin
kind: GitConfigMapGenerator
metadata:
  name: configuration-map
  annotations:
    config.kubernetes.io/function: |
      exec:
        path: krmfnbuiltin
remoteName: origin # default

produces the following config map (comments mine):

apiVersion: v1
kind: ConfigMap
metadata:
  name: configuration-map
  namespace: argocd
  annotations:
    # add config.kaweezle.com/prune-local: "true" to last transformer to remove
    config.kubernetes.io/local-config: "true"
    # Add .generated.yaml to .gitignore to avoid mistakes
    internal.config.kubernetes.io/path: .generated.yaml
    config.kubernetes.io/path: .generated.yaml
data:
  repoURL: git@github.com:kaweezle/krmfnbuiltin.git
  targetRevision: feature/extended-replacement-transformer

Heredoc generator

We have seen in Use of generators how to use ConfigMapGenerator to inject values in order to use them in downstream transformers, replacements in particular. It has however some limitations, due to the flat nature of ConfigMaps and the fact that values are only strings. The former makes it difficult to organize replacement variables and the later prevents structural (object) replacement. For object replacements we can use PatchStrategicMergeTransformer, but then we loose the ReplacementTransformer advantage of using the same source for several targets and end up having duplicate YAML snippets.

krmfnbuiltin allows injecting any KRM resource in the transformation by just adding the config.kaweezle.com/inject-local: "true" annotation to the function configuration. For instance:

apiVersion: config.kaweezle.com/v1alpha1
kind: LocalConfiguration
metadata:
  name: traefik-customization
  annotations:
    # This will inject this resource. like a ConfigMapGenerator, but with hierarchical
    # properties
    config.kaweezle.com/inject-local: "true"
    # This annotation will allow pruning at the end
    config.kaweezle.com/local-config: "true"
    config.kubernetes.io/function: |
      exec:
        path: krmfnbuiltin
data:
  # kustomization
  traefik:
    dashboard_enabled: true
    expose: true
  sish:
    # New properties
    server: target.link
    hostname: myhost.target.link
    host_key: AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAID+4/eqtPTLC18TE8ZP7NeF4ZP68/wnY2d7mhH/KVs79AAAABHNzaDo=

When the function configuration contains the config.kaweezle.com/inject-local, annotation, krmfnbuiltin bypasses the generation/transformation process for this function and return the content of the function config as if it had been generated. The config.kaweezle.com/inject-local annotation as well as the config.kubernetes.io/function annotation are removed from the injected resource.

The resource contents can then be used in the following transformations, in particular in replacements, and deleted at the end (with config.kaweezle.com/local-config and config.kaweezle.com/prune-local) or even saved (with config.kaweezle.com/path). See Keeping or deleting generated resources) for more details.

Kustomization generator

KustomizationGenerator is the kustomize equivalent to HelmChartInflationGenerator. It allows generating resources from a kustomization.

Example:

apiVersion: builtin
kind: KustomizationGenerator
metadata:
  name: kustomization-generator
  annotations:
    config.kaweezle.com/path: "uninode.yaml" # file name to save resources
    config.kubernetes.io/function: |
      exec:
        path: krmfnbuiltin
kustomizeDirectory: https://github.com/antoinemartin/autocloud.git//packages/uninode?ref=deploy/citest

If this function is run with the following command:

> kustomize fn run --enable-exec --fn-path functions applications

It will generate a file named uninode.yaml containing all the resources of the built kustomization in the applications directory. With:

config.kaweezle.com/path: ""

One file will be created per resource (see Keeping or deleting generated resources).

IMPORTANT The current directory krmfnbuiltin runs from is the directory in which the kustomize run fn command has been launched, and not from the function configuration folder. Any relative path should take this into consideration.

Sops decryption generator

The SopsGenerator generates resources from encrypted content. This content can be the actual function configuration, in heredoc style, or can come from other files.

It is an inclusion of krmfnsops. See its README file for more information.

In the simplest use case, Imagine you have an unencrypted secret that looks like this :

# argocd-secret.yaml
apiVersion: v1
kind: Secret
type: Opaque
metadata:
  name: argocd-secret
stringData:
  admin.password: $2a$10$xdlX460lf/WbJNZU5bBoROj6U7oKgPbEcBrnXaemA6gsCzrAJtQ3y
  admin.passwordMtime: "2022-08-30T11:26:42Z"
  webhook.github.secret: ZxqGggxGD070l3dx
  dex.github.clientSecret: 7lqt6nasit6kjtvptmy2dzy1dr796orn5xh05ru1

If you encrypt it with sops, you get something like this:

# argocd-secret.yaml
apiVersion: v1
kind: Secret
type: Opaque
metadata:
  name: argocd-secret
stringData:
  admin.password: ENC[AES256_GCM,data:...,type:str]
  admin.passwordMtime: ENC[AES256_GCM,data:...,type:str]
  webhook.github.secret: ENC[AES256_GCM,data:...,type:str]
  dex.github.clientSecret: ENC[AES256_GCM,data:...==,type:str]
sops:
  age:
    - recipient: age166k86d56...
      enc: |
        -----BEGIN AGE ENCRYPTED FILE-----
        ...
        -----END AGE ENCRYPTED FILE-----
  lastmodified: "2023-02-06T11:36:44Z"
  mac: ENC[AES256_GCM,data:...,type:str]
  pgp: []
  encrypted_regex: ^(data|stringData|.*_keys?|admin|adminKey|password)$
  version: 3.7.3

If you want this resource to be unencrypted at kustomization build, you can create the following generator configuration:

# argocd-secret-generator.yaml
apiVersion: krmfnbuiltin.kaweezle.com/v1alpha1
kind: SopsGenerator
metadata:
  name: argocd-secret-generator
  annotations:
    config.kubernetes.io/function: |
      exec:
        path: krmfnbuiltin
files:
  - argocd-secret.yaml

And insert it in the generators: section of your kustomization.yaml file:

generators:
  - argocd-secret-generator.yaml

To avoid adding a generator configuration file to your kustomization, you can directly transform the encrypted secret file into a KRM generator:

# argocd-secret.yaml
apiVersion: krmfnbuiltin.kaweezle.com/v1alpha1
kind: SopsGenerator
type: Opaque
metadata:
  name: argocd-secret
  annotations:
    config.kaweezle.com/kind: "Secret"
    config.kaweezle.com/apiVersion: "v1"
    config.kubernetes.io/function: |
      exec:
        path: krmfnbuiltin
stringData:
  admin.password: ENC[AES256_GCM,data:...,type:str]
  admin.passwordMtime: ENC[AES256_GCM,data:...,type:str]
  webhook.github.secret: ENC[AES256_GCM,data:...,type:str]
  dex.github.clientSecret: ENC[AES256_GCM,data:...==,type:str]
sops:
  age:
    - recipient: age166k86d56...
      enc: |
        -----BEGIN AGE ENCRYPTED FILE-----
        ...
        -----END AGE ENCRYPTED FILE-----
  lastmodified: "2023-02-06T11:36:44Z"
  mac: ENC[AES256_GCM,data:...,type:str]
  pgp: []
  encrypted_regex: ^(data|stringData|.*_keys?|admin|adminKey|password)$
  version: 3.7.3

And your kustomization.yaml file would look like:

generators:
  - argocd-secret.yaml

Note the use of the following annotations:

config.kaweezle.com/kind: "Secret"
config.kaweezle.com/apiVersion: "v1"

In order to have the generated resource with the proper kind and api version.

WARNING While this second inclusion method reduces the number of files, it disables the sops Message authentication code (MAC) verification that prevents file tampering. Use it at your own risk.

Extended replacement in structured content

The ReplacementTransformer provided in krmfnbuiltin is extended compared to the standard one because it allows structured replacements in properties containing a string representation of some structured content. It currently supports the following structured formats:

  • YAML
  • JSON
  • TOML
  • INI

It also provides helpers for changing content in base64 encoded properties as well as a simple regexp based replacer for edge cases. The standard configuration of the transformer can be found in the replacements kustomize documentation.

The typical use case for this is when you have an Argo CD application using a Helm chart as source with some custom values:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: traefik
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  destination:
    namespace: traefik
    server: https://kubernetes.default.svc
  project: default
  source:
    chart: traefik
    repoURL: https://helm.traefik.io/traefik
    targetRevision: "10.19.5"
    helm:
      parameters: []
      values: |-
        ingressClass:
          enabled: true
          isDefaultClass: true
        ingressRoute:
          dashboard:
            enabled: false
        providers:
          kubernetesCRD:
            allowCrossNamespace: true
            allowExternalNameServices: true
          kubernetesIngress:
            allowExternalNameServices: true
            publishedService:
              enabled: true
        logs:
          general:
            level: ERROR
          access:
            enabled: true
        tracing:
          instana: false
        gobalArguments: {}
        # BEWARE: use only for debugging
        additionalArguments:
         - --api.insecure=false
        ports:
          # BEWARE: use only for debugging
          # traefik:
          #   expose: false
          web:
            redirectTo: websecure
          websecure:
            tls:
              enabled: true
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
  ignoreDifferences: []

And that you want your KRM function to personalize the values of the Helm chart. What you would want is having your replacement path follow inside the values property by specifying:

- spec.source.helm.values.<inside>.ingressRoute.dashboard.enabled

This is not possible with the standard ReplacementTransformer, but this is is possible with the one provided by krmfnbuiltin. Consider the following function configurations:

# fn-traefik-customization.yaml
apiVersion: builtin
kind: LocalConfiguration
metadata:
  name: traefik-customization
  annotations:
    # This will inject this resource. like a ConfigMapGenerator, but with hierarchical
    # properties
    config.kaweezle.com/inject-local: "true"
    config.kaweezle.com/local-config: "true"
    config.kubernetes.io/function: |
      exec:
        path: krmfnbuiltin
data:
  # kustomization
  traefik:
    dashboard_enabled: true
    expose: true
---
apiVersion: builtin
kind: ReplacementTransformer
metadata:
  name: replacement-transformer
  annotations:
    # remove LocalConfiguration after
    config.kaweezle.com/prune-local: "true"
    config.kubernetes.io/function: |
      exec:
        path: krmfnbuiltin
replacements:
  - source:
      kind: LocalConfiguration
      fieldPath: data.traefik.dashboard_enabled
    targets:
      - select:
          kind: Application
          name: traefik
        fieldPaths:
          # !!yaml tells the transformer that the property contains YAML
          - spec.source.helm.values.!!yaml.ingressRoute.dashboard.enabled
  - source:
      kind: LocalConfiguration
      fieldPath: data.traefik.expose
    targets:
      - select:
          kind: Application
          name: traefik
        fieldPaths:
          - spec.source.helm.values.!!yaml.ports.traefik.expose

If you apply this to the directory containing the application, you will obtain a new application:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: traefik
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
  annotations:
    config.kubernetes.io/path: traefik.yaml
    internal.config.kubernetes.io/path: traefik.yaml
spec:
  destination:
    namespace: traefik
    server: https://kubernetes.default.svc
  project: default
  source:
    chart: traefik
    helm:
      parameters: []
      values: |
        ...
        ingressRoute:
          dashboard:
            enabled: true
        ...
        ports:
          # BEWARE: use only for debugging
          # traefik:
          #   expose: false
          web:
            redirectTo: websecure
          websecure:
            tls:
              enabled: true
          traefik:
            expose: true
    repoURL: https://helm.traefik.io/traefik
    targetRevision: "10.19.5"
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
  ignoreDifferences: []

As you can see, inside the values property, the yaml has been modified. ingressRoute.dashboard.enabled is now true and port.traefik.expose is also true. Notice that this last property, also present as a comment, has been inserted at the end of the ports section.

Now for a more extreme use case involving regular expressions, imagine you have the following configuration map defining two files:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: sish-client
  namespace: traefik
  labels:
    app.kubernetes.io/name: "sish-client"
    app.kubernetes.io/component: edge
    app.kubernetes.io/part-of: autocloud
data:
  # ~/.ssh/config file
  config: |
    PubkeyAcceptedKeyTypes +ssh-rsa
    Host sishserver
      HostName holepunch.in
      Port 2222
      BatchMode yes
      IdentityFile ~/.ssh_keys/id_rsa
      IdentitiesOnly yes
      LogLevel ERROR
      ServerAliveInterval 10
      ServerAliveCountMax 2
      RemoteCommand sni-proxy=true
      RemoteForward citest.holepunch.in:443 traefik.traefik.svc:443
  # ~/.ssh/known_hosts with the server key
  known_hosts: |
    [holepunch.in]:2222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID+3abW2y3T5dodnI5O1Z/2KlIdH3bwnbGDvCFf13zlh

And imagine you want to modify it to access a different server on another domain name. You need to change:

  • HostName in ~/.ssh/config from holepunch.in to the new server address.
  • RemoteForward in ~/.ssh/config by changing the address forwarded from citest.holepunch.in to the new address.
  • In ~/.ssh/known_hosts the name of the host and the key fingerprint of the new server.

You can do this by hand, but you may forget something now and the next time. This is where the regexp transformer comes into play with the following configuration:

apiVersion: builtin
kind: LocalConfiguration
metadata:
  name: configuration-map
  annotations:
    config.kaweezle.com/inject-local: "true"
    config.kaweezle.com/local-config: "true"
    config.kubernetes.io/function: |
      exec:
        path: krmfnbuiltin
data:
  sish:
    # New properties
    server: target.link
    hostname: myhost.target.link
    host_key: AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAID+4/eqtPTLC18TE8ZP7NeF4ZP68/wnY2d7mhH/KVs79AAAABHNzaDo=
---
apiVersion: builtin
kind: ReplacementTransformer
metadata:
  name: replacement-transformer
  annotations:
    config.kaweezle.com/prune-local: "true"
    config.kubernetes.io/function: |
      exec:
        path: krmfnbuiltin
replacements:
  - source:
      kind: LocalConfiguration
      fieldPath: data.sish.server
    targets:
      - select:
          kind: ConfigMap
          name: sish-client
        fieldPaths:
          - data.config.!!regex.^\s+HostName\s+(\S+)\s*$.1
          - data.known_hosts.!!regex.^\[(\S+)\].1
  - source:
      kind: LocalConfiguration
      fieldPath: data.sish.hostname
    targets:
      - select:
          kind: ConfigMap
          name: sish-client
        fieldPaths:
          - data.config.!!regex.^\s+RemoteForward\s+(\S+):.1
  - source:
      kind: LocalConfiguration
      fieldPath: data.sish.host_key
    targets:
      - select:
          kind: ConfigMap
          name: sish-client
        fieldPaths:
          - data.known_hosts.!!regex.ssh-ed25519\s(\S+).1

The path after !!regex is composed of two elements. The first one is the regexp to match. The second one is the the capture group that needs to be replaced with the source. In the first replacement, the regexp:

^\s+HostName\s+(\S+)\s*$

can be interpreted as:

a line starting with one or more spaces followed by HostName, then one or more spaces and a sequence of non space characters, captured as a group; then optional spaces till the end of the line.

The second part of the path, 1, tells to replace the first capturing group with the source. With the above, the line:

      HostName holepunch.in

will become

      HostName target.link

Replacements source reuse

In the above examples, the ReplacementTransformer gets the source data from a generator that is injected (config.kaweezle.com/inject-local: "true") and then removed (config.kaweezle.com/prune-local: "true"). The extended version of ReplacementTransformer allows specifying a source: that can either be a resource file or the path of a kustomization.

We can create a properties.yaml file:

# properties.yaml
apiVersion: autocloud.config.kaweezle.com/v1alpha1
kind: PlatformValues
metadata:
  name: autocloud-values
data:
  traefik:
    dashboard_enabled: true
    expose: true
  sish:
    hostname: mydomain.link
    remote: argocd.mydomain.link
    host_key: AAAAC3NzaC1lZDI1NTE5AAAAIEAfLUpTj0fn5sJFW6agmLMsvEacMBvXocyzHLW+AOSQ
    # more configuration below...

And then reference it from our replacements:

# fn-traefik-customization.yaml
apiVersion: builtin
kind: ReplacementTransformer
metadata:
  name: replacement-transformer
  annotations:
    config.kubernetes.io/function: |
      exec:
        path: krmfnbuiltin
# Source of replacements
source: properties.yaml
replacements:
  - source:
      kind: LocalConfiguration
      fieldPath: data.traefik.dashboard_enabled
    targets:
      - select:
          kind: Application
          name: traefik
        fieldPaths:
          # !!yaml tells the transformer that the property contains YAML
          - spec.source.helm.values.!!yaml.ingressRoute.dashboard.enabled

As the source of the replacement is side loaded, there no need to inject it nor remove it from the configuration. Also, as the source can be a kustomization, there is no need for it to be local.

Replacement with encoding

Kustomize has an encoding option in ReplacementTransformer that is currently unused. We put it to the work and provide three encoding types:

  • base64
  • bcrypt
  • hex

Example:

- source:
    name: autocloud-values
    fieldPath: data.to_encode
    options:
      encoding: base64
  targets:
    - select:
        kind: ConfigMap
        name: argocd-cm
      fieldPaths:
        - data.b64encoded

Thanks to this feature, you can keep some values in clear text inside your properties files and encode them on kustomization. Be aware that the bcrypt encoding will generate a new value for each kustomization.

Installation

With each Release, we provide binaries for most platforms as well as Alpine based packages.

On POSIX systems (Linux and Mac), you can install the last version with:

curl -sLS https://raw.githubusercontent.com/kaweezle/krmfnbuiltin/main/get.sh | /bin/sh

If you don't want to pipe into shell, you can do:

> KRMFNBUILTIN_VERSION="v0.4.1"
> curl -sLo /usr/local/bin/krmfnbuiltin https://github.com/kaweezle/krmfnbuiltin/releases/download/${KRMFNBUILTIN_VERSION}/krmfnbuiltin_${KRMFNBUILTIN_VERSION}_linux_amd64

Argo CD integration

krmfnbuiltin is NOT primarily meant to be used inside Argo CD, but instead to perform structural modifications to the configuration BEFORE it's committed and provided to GitOps.

Anyway, to use krmfnbuiltin with Argo CD, you need to:

  • Make the krmfnbuiltin binary available to the argo-repo-server pod.
  • Have Argo CD run kustomize with the --enable-alpha-plugins --enable-exec parameters.

To add krmfnbuiltin on argo-repo-server, the Argo CD documentation provides different methods to make custom tools available.

If you get serious about Argo CD, you will probably end up cooking your own image. This docker file shows how to use the above installation instructions in your image. To summarize:

FROM argoproj/argocd:latest

ARG KRMFNBUILTIN_VERSION=v0.4.1

# Switch to root for the ability to perform install
USER root

# Install tools
RUN apt-get update && \
    apt-get install -y curl && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* && \
    curl -sLo /usr/local/bin/krmfnbuiltin https://github.com/kaweezle/krmfnbuiltin/releases/download/${KRMFNBUILTIN_VERSION}/krmfnbuiltin_${KRMFNBUILTIN_VERSION}_linux_amd64

USER argocd

You also need to patch the argo-cm config map to add the parameters. The following is a strategic merge patch for it:

# argocd-cm.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
data:
  # Options to enable exec plugins (krmfnsops).
  kustomize.buildOptions: "--enable-alpha-plugins --enable-exec"
  ...

Related projects

kpt, from Google, takes this in place transformation principle to another level by making resource configuration packages similar to docker images. In this model, a generator or a transformer along its parameters in much like a line in a dockerfile. It takes a current configuration as source and generates a new configuration after transformation.

krmfnbuiltin works with kpt. The tests/test_krmfnbuiltin_kpt.sh script perform the basic tests with kpt.

knot8 lenses have provided the idea of extended paths.