Skip to content

Latest commit

 

History

127 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

karmafun

stability-beta

karmafun 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. karmafun build
  7. Installation
  8. Argo CD integration
  9. 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 .

karmafun 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.karmafun.dev/cleanup: "true"
    config.kubernetes.io/function: |
      exec:
        path: karmafun
    # Can also be:
    #  container:
    #    image: ghcr.io/karmafun/karmafun: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.karmafun.dev/cleanup: "true"

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

Use of generators

karmafun 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.karmafun.dev/local-config: "true"
    config.kubernetes.io/function: |
      exec:
        path: karmafun
# 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.karmafun.dev/prune-local: "true"
    config.kubernetes.io/function: |
      exec:
        path: karmafun
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.karmafun.dev/local-config: "true"

    that is injected in the generated resource.

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

    config.karmafun.dev/prune-local: "true"

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

Keeping or deleting generated resources

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

config.karmafun.dev/local-config: "true"

on the generators and:

config.karmafun.dev/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 .karmafun.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.karmafun.dev/local-config annotation.

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

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

Example:

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

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.karmafun.dev/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 karmafun 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 karmafun, the config.karmafun.dev/prune-local may not be available to remove resources injected in the transformation pipeline. For this use case, karmafun provides RemoveTransformer:

apiVersion: builtin
kind: RemoveTransformer
metadata:
  name: replacement-transformer
  annotations:
    config.kubernetes.io/function: |
      exec:
        path: ../../karmafun
targets:
  - annotationSelector: config.karmafun.dev/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: karmafun
remoteName: origin # default

produces the following config map (comments mine):

apiVersion: v1
kind: ConfigMap
metadata:
  name: configuration-map
  namespace: argocd
  annotations:
    # add config.karmafun.dev/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:karmafun/karmafun.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.

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

apiVersion: config.karmafun.dev/v1alpha1
kind: LocalConfiguration
metadata:
  name: traefik-customization
  annotations:
    # This will inject this resource. like a ConfigMapGenerator, but with hierarchical
    # properties
    config.karmafun.dev/inject-local: "true"
    # This annotation will allow pruning at the end
    config.karmafun.dev/local-config: "true"
    config.kubernetes.io/function: |
      exec:
        path: karmafun
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.karmafun.dev/inject-local, annotation, karmafun bypasses the generation/transformation process for this function and return the content of the function config as if it had been generated. The config.karmafun.dev/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.karmafun.dev/local-config and config.karmafun.dev/prune-local) or even saved (with config.karmafun.dev/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.karmafun.dev/path: "uninode.yaml" # file name to save resources
    config.kubernetes.io/function: |
      exec:
        path: karmafun
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.karmafun.dev/path: ""

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

IMPORTANT The current directory karmafun 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: karmafun.dev/v1alpha1
kind: SopsGenerator
metadata:
  name: argocd-secret-generator
  annotations:
    config.kubernetes.io/function: |
      exec:
        path: karmafun
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: karmafun.dev/v1alpha1
kind: SopsGenerator
type: Opaque
metadata:
  name: argocd-secret
  annotations:
    config.karmafun.dev/kind: "Secret"
    config.karmafun.dev/apiVersion: "v1"
    config.kubernetes.io/function: |
      exec:
        path: karmafun
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.karmafun.dev/kind: "Secret"
config.karmafun.dev/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.

KCL Generator

The KCL Generator (KCLRun) generates Kubernetes resources from KCL (Kusion Configuration Language) code. KCL is a constraint-based record and functional programming language designed for configuration and policy scenarios.

Inline KCL Code

You can write KCL code directly in the function configuration:

apiVersion: kcl.dev/v1alpha1
kind: KCLRun
metadata:
  name: example-kcl-run
  annotations:
    config.kubernetes.io/function: |
      exec:
        path: karmafun
spec:
  source: |
    {
      apiVersion = "v1"
      kind = "ConfigMap"
      metadata.name = "example-configmap"
      data.key = "value"
    }

This will generate a ConfigMap resource with the specified configuration.

KCL Code from File

You can also reference an external KCL file:

apiVersion: kcl.dev/v1alpha1
kind: KCLRun
metadata:
  name: example-kcl-run
  annotations:
    config.kubernetes.io/function: |
      exec:
        path: karmafun
spec:
  source: ./kcl/configmap.k

Where configmap.k contains your KCL code:

{
  apiVersion = "v1"
  kind = "ConfigMap"
  metadata.name = "example-configmap"
  data.key = "value"
}

NOTE: The path is relative to the directory where kustomize fn run is executed, not the function configuration directory.

KCL Transformer

The KCL Transformer (KCLTransformer) transforms existing Kubernetes resources using KCL code. This is particularly useful for complex transformations that are difficult to express with standard transformers.

Example: Generate Certificates

apiVersion: kcl.dev/v1alpha1
kind: KCLTransformer
metadata:
  name: generate-certificates
  annotations:
    config.karmafun.dev/prune-local: "true"
    config.kubernetes.io/function: |
      exec:
        path: karmafun
spec:
  dependencies: |
    cert-manager = "0.3.0"
  source: ./functions/transformer.k

The transformer can:

  • Read existing resources from the input
  • Apply KCL-based transformations
  • Generate new resources based on existing ones
  • Use KCL libraries and dependencies (specified in the dependencies field)

You can find more information about the configuration of the transformer in the krm-kcl site and in the KCL documentation.

Using KCL Dependencies

The dependencies field allows you to use external KCL modules. In the example above, cert-manager = "0.3.0" makes the cert-manager KCL module available to your transformation code.

TIP: Use the config.karmafun.dev/prune-local annotation to remove the transformer configuration from the output if you don't want it saved.

Extended replacement in structured content

The ReplacementTransformer provided in karmafun 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
        globalArguments: {}
        # 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 karmafun. 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.karmafun.dev/inject-local: "true"
    config.karmafun.dev/local-config: "true"
    config.kubernetes.io/function: |
      exec:
        path: karmafun
data:
  # kustomization
  traefik:
    dashboard_enabled: true
    expose: true
---
apiVersion: builtin
kind: ReplacementTransformer
metadata:
  name: replacement-transformer
  annotations:
    # remove LocalConfiguration after
    config.karmafun.dev/prune-local: "true"
    config.kubernetes.io/function: |
      exec:
        path: karmafun
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.karmafun.dev/inject-local: "true"
    config.karmafun.dev/local-config: "true"
    config.kubernetes.io/function: |
      exec:
        path: karmafun
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.karmafun.dev/prune-local: "true"
    config.kubernetes.io/function: |
      exec:
        path: karmafun
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.karmafun.dev/inject-local: "true") and then removed (config.karmafun.dev/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.karmafun.dev/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: karmafun
# 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.

karmafun build

karmafun build is a kustomize build replacement that extends the standard build pipeline with two additional capabilities:

  • Values and secrets injection — a values.yaml file and an optional SOPS‑encrypted secrets.sops.yaml file are loaded, merged, and made available inside kustomization files as Go template variables.
  • Go template rendering — any file in the kustomization directory whose name ends with .tmpl or .gotmpl is rendered as a Go text/template before being passed to kustomize. All Sprig functions are available.

All karmafun plugins (and any standard kustomize plugin) can be used in the kustomization, so you can combine template-driven resource generation with transformations such as ReplacementTransformer, GitConfigMapGenerator, or SopsGenerator.

Usage

karmafun build [flags] <kustomization directory>
Flag Default Description
--values-file values.yaml YAML file containing plain-text platform values
--secrets-file secrets.sops.yaml SOPS‑encrypted YAML file containing secret values
--output-directory / -o (stdout) Write each resource to its own Kind-name.yaml file in this directory
--log-level info Log level (debug, info, warn, error)
--log-json false Emit logs in JSON format

When no --output-directory is given, the built YAML is printed to stdout, just like kustomize build.

Values and secrets

Both the values file and the secrets file use the PlatformValues resource format:

# values.yaml
apiVersion: config.karmafun.dev/v1alpha1
kind: PlatformValues
metadata:
  name: my-values
data:
  domain_suffix: example.com
  project:
    name: my-project
  argocd:
    target_revision: main

The secrets file has exactly the same structure, but it must be encrypted with sops. The two files are deep-merged at build time, with secrets values taking precedence over plain values.

To create an encrypted secrets file from an unencrypted one:

sops -e secrets.dec.sops.yaml > secrets.sops.yaml

If either file is absent, it is silently ignored and treated as empty.

Go template support

Any kustomization resource file ending with .tmpl or .gotmpl is transparently rendered as a Go template before kustomize processes it. The merged values are accessible under the .Values variable:

Template expression Description
{{ .Values.data.key }} Top‑level value key
{{ .Values.data.nested.key }} Nested value key

All Sprig helper functions (upper, trim, toJson, …) are available in templates.

Example template file application.yaml.gotmpl:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  source:
    repoURL:
      git@github.com:{{ .Values.data.project.github.organization }}/{{
      .Values.data.project.github.repo }}.git
    targetRevision: { { .Values.data.argocd.target_revision } }
    path: deploy/k8s/{{ .Values.data.argocd.base_path }}

The rendered output is what kustomize sees, so the final resource contains the concrete values from your values.yaml / secrets.sops.yaml.

Sample kustomization

The samples/kustomization directory contains a self-contained example that demonstrates the full workflow:

samples/kustomization/
├── appstage-00-bootstrap/
│   ├── application.yaml.gotmpl   # Go template — uses .Values.data.*
│   └── kustomization.yaml        # Kustomization that references the template
│                                 # and the secrets generator
├── values.yaml                   # Plain-text PlatformValues resource
├── secrets.dec.sops.yaml         # Unencrypted secrets (for reference only)
├── secrets.sops.yaml             # SOPS-encrypted secrets
└── test_kustomization.sh         # Script that runs karmafun build

To run the sample (after installing karmafun):

# Extract the sample age key so sops can decrypt secrets.sops.yaml
gojq -r --yaml-input '.data.sops["age_key.txt"]' \
  ./samples/kustomization/secrets.dec.sops.yaml \
  >> ~/.config/sops/age/keys.txt

# Build the sample kustomization
karmafun build samples/kustomization/appstage-00-bootstrap

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/karmafun/karmafun/main/get.sh | /bin/sh

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

> KARMAFUN_VERSION="v0.4.1"
> curl -sLo /usr/local/bin/karmafun https://github.com/karmafun/karmafun/releases/download/${KARMAFUN_VERSION}/karmafun_${KARMAFUN_VERSION}_linux_amd64

Argo CD integration

karmafun 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 karmafun with Argo CD, you need to:

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

To add karmafun 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 KARMAFUN_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/karmafun https://github.com/karmafun/karmafun/releases/download/${KARMAFUN_VERSION}/karmafun_${KARMAFUN_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.

karmafun works with kpt. The tests/test_karmafun_kpt.sh script perform the basic tests with kpt.

knot8 lenses have provided the idea of extended paths.

About

Run kustomize builtins via kustomize fn , make smart/secrets replacements and more

Topics

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages