Add another detailed plugin example.

This commit is contained in:
Jeffrey Regan
2019-06-24 13:36:06 -07:00
parent ae5ebccec7
commit 1740ca6a16
13 changed files with 621 additions and 249 deletions

353
docs/plugins/README.md Normal file
View File

@@ -0,0 +1,353 @@
# kustomize plugins
Quick guides:
* [linux exec plugin in 60 sec](execPluginGuidedExample.md)
* [linux Go plugin in 60 sec](goPluginGuidedExample.md)
Kustomize offers a plugin framework allowing
people to write their own resource _generators_
and _transformers_.
[generator options]: ../../examples/generatorOptions.md
[transformer configs]: ../../examples/transformerconfigs
Write a plugin when changing [generator options]
or [transformer configs] doesn't meet your needs.
[12-factor]: https://12factor.net
* A _generator_ plugin could be a helm chart
inflator, or a plugin that emits all the
components (deployment, service, scaler,
ingress, etc.) needed by someone's [12-factor]
application, based on a smaller number of free
variables.
* A _transformer_ plugin might perform special
container command line edits, or any other
transformation that exceeds the power of the
builtin transformations (`namePrefix`,
`commonLabels`, etc.).
## Specification in `kustomization.yaml`
Start by adding a `generators` and/or `transformers`
field to your kustomization.
Each field accepts a string list:
> ```
> generators:
> - relative/path/to/some/file.yaml
> - relative/path/to/some/kustomization
> - /absolute/path/to/some/kustomization
> - https://github.com/org/repo/some/kustomization
>
> transformers:
> - {as above}
> ```
This is exactly like the syntax of the `resources`
field.
The value of each entry in a `resources`,
`generators` or `transformers` list must be a
relative path to a YAML file, or a path or URL
to a [kustomization].
[kustomization]: ../glossary.md#kustomization
In the former case the YAML is read from disk directly,
and in the latter case a kustomization is performed,
and its YAML output is merged with the YAML read
directly from files. The net result in all three cases
is a set of YAML objects.
Each object resulting from a `generators` or
`transformers` field is now further interpreted by
kustomize as a _plugin configuration_ object.
## Configuration
A kustomization file could have the following lines:
```
generators:
- chartInflator.yaml
```
Given this, the kustomization process would expect to
find a file called `chartInflator.yaml` in the
kustomization [root](../glossary.md#kustomization-root).
This is the _plugin's configuration file_.
The file `chartInflator.yaml` could contain:
```
apiVersion: someteam.example.com/v1
kind: ChartInflator
metadata:
name: notImportantHere
chartName: minecraft
```
__The `apiVersion` and `kind` fields are
used to locate the plugin.__
[k8s object]: ../glossary.md#kubernetes-style-object
> Thus, these fields are required. They are also
> required because a kustomize plugin
> configuration object is also a [k8s object].
To get the plugin ready to generator or transform,
it is given the entire contents of the
configuration file.
[NameTransformer]: ../../plugin/builtin/prefixsuffixtransformer/PrefixSuffixTransformer_test.go
[ChartInflator]: ../../plugin/someteam.example.com/v1/chartinflator/ChartInflator_test.go
[plugins]: ../../plugin/builtin
For more examples of plugin configuration YAML,
browse the unit tests below the [plugins] root,
e.g. the tests for [ChartInflator] or
[NameTransformer].
## Placement
Each plugin gets its own dedicated directory named
```
$XDG_CONFIG_HOME/kustomize/plugin
/${apiVersion}/LOWERCASE(${kind})
```
The default value of `XDG_CONFIG_HOME` is
`$HOME/.config`.
The one-plugin-per-directory requirement eases
creation of a plugin tarball (source, test, plugin
data files, etc.) for sharing.
In the case of a [Go plugin](#go-plugins), it also
allows one to provide a `go.mod` file for the
single plugin, easing resolution of package
version dependency skew.
When loading, kustomize will first look for an
_executable_ file called
```
$XDG_CONFIG_HOME/kustomize/plugin
/${apiVersion}/LOWERCASE(${kind})/${kind}
```
If this file is not found or is not executable,
kustomize will look for a file called `${kind}.so`
in the same directory and attempt to load it as a
[Go plugin](#go-plugins).
If both checks fail, the plugin load fails the overall
`kustomize build`.
## Execution
Plugins are only used during a run of the
`kustomize build` command.
Generator plugins are run after processing the
`resources` field (which itself is in some sense a
generator in that it emits resources for further
processing).
The full set of resources is then passed into the
transformation pipeline, wherein builtin
transformations like `namePrefix` and
`commonLabel` are applied (if they were specified
in the kustomization file), followed by the
user-specified transformers in the `transformers`
field.
The specified order of transformers in the
`transformers` field should be respected, as
transformers cannot be expected to be commutative.
A `kustomize build` that tries to use plugins but
omits the flag
> `--enable_alpha_plugins`
will fail with a warning about plugin use.
Flag use is an opt-in acknowledging the absence of
plugin provenance. It's meant to give pause to
someone who blindly downloads a kustomization from
the internet and attempts to run it, without
realizing that it might attempt to run 3rd party
code in plugin form. The plugin would have to be
installed already, but nevertheless the flag is a
reminder.
## Writing plugins
### Exec plugins
A _exec plugin_ is any executable that accepts a
single argument on its command line - the name of
a YAML file containing its configuration (the file name
provided in the kustomization file).
> TODO: more restrictions on plugin to allow the same exec
> plugin to be specified in a config under both the
> `generators` and `transformers` fields.
> - first arg could be the fixed string
> `generate` or `transform`,
> (the name of the configuration file moves to
> the 2nd arg), or
> - by default an exec plugin behaves as a tranformer
> unless a flag `-g` is provided, switching the
> exec plugin to behave as a generator.
[helm chart inflator]: ../../plugin/someteam.example.com/v1/chartinflator
[bashed config map]: ../../plugin/someteam.example.com/v1/bashedconfigmap
[sed transformer]: ../../plugin/someteam.example.com/v1/sedtransformer
#### Examples
* [helm chart inflator] - A generator that inflates a helm chart.
* [bashed config map] - Super simple configMap generation from bash.
* [sed transformer] - Define your unstructured edits using a
plugin like this one.
A generator plugin accepts nothing on `stdin`, but emits
generated resources to `stdout`.
A transformer plugin accepts resource YAML on `stdin`,
and emits those resources, presumably transformed, to
`stdout`.
kustomize uses an exec plugin adapter to provide
marshalled resources on `stdin` and capture
`stdout` for further processing.
### Go plugins
[Go plugin]: https://golang.org/pkg/plugin/
A [Go plugin] for kustomize looks like this:
> ```
> package main
>
> import (
> "sigs.k8s.io/kustomize/v3/pkg/ifc"
> "sigs.k8s.io/kustomize/v3/pkg/resmap"
> ...
> )
>
> type plugin struct {...}
>
> var KustomizePlugin plugin
>
> func (p *plugin) Config(
> ldr ifc.Loader,
> rf *resmap.Factory,
> c []byte) error {...}
>
> func (p *plugin) Generate() (resmap.ResMap, error) {...}
>
> func (p *plugin) Transform(m resmap.ResMap) error {...}
> ```
Use of the identifiers `plugin`, `KustomizePlugin`
and implementation of the method signature
`Config` is required.
Implementing the `Generator` or `Transformer`
method allows (respectively) the plugin's config
file to be added to the `generators` or
`transformers` field in the kustomization file.
Do one or the other or both as desired.
[secret generator]: ../../plugin/someteam.example.com/v1/secretsfromdatabase
[service generator]: ../../plugin/someteam.example.com/v1/someservicegenerator
[string prefixer]: ../../plugin/someteam.example.com/v1/stringprefixer
[date prefixer]: ../../plugin/someteam.example.com/v1/dateprefixer
#### Examples
* [secret generator] - Generate secrets from a database.
* [service generator] - Generate a service from a name and port argument.
* [string prefixer] - uses the value in `metadata/name` as the prefix.
This particular example exists to show how a plugin can
transform the behavior of a plugin. See the
`TestTransformedTransformers` test in the `target` package.
* [date prefixer] - prefix the current date to resource names, a simple
example used to modify the string prefixer plugin just mentioned.
* All the builtin plugins [here](../../plugin/builtin).
User authored plugins are
on the same footing as builtin operations.
A plugin can be both a generator and a
transformer. The `Generate` method will run along
with all the other generators before the
`Transform` method runs.
Here's a build command that sensibly assumes the
plugin source code sits in the directory where
kustomize expects to find `.so` files:
```
d=$XDG_CONFIG_HOME/kustomize/plugin\
/${apiVersion}/LOWERCASE(${kind})
go build -buildmode plugin \
-o $d/${kind}.so $d/${kind}.go
```
#### Caveats
Go plugins allow kustomize extensions that run
without the cost marshalling/unmarshalling all
resource data to/from a subprocess for each plugin
run.
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
Go plugins work as [defined][Go plugin], but fall
short of common notions associated with the word
_plugin_. Go plugin compilation creates an [ELF]
formatted `.so` file, which by definition has no
information about the provenance of the object
code. Skew between the compilation conditions
(versions of package dependencies, `GOOS`,
`GOARCH`) of the main program ELF and the plugin
ELF will cause plugin load failure.
Exec plugins also lack provenance, but won't
complain about compilation skew.
In either case, a sensible way to share a plugin
is as a tar file of source code, tests and
associated data, unpackable under
`$XDG_CONFIG_HOME/kustomize/plugin` (exactly where
one would develop a plugin).
[Go modules]: https://github.com/golang/go/wiki/Modules
In the case of a Go plugin, an end user accepting
a shared plugin must compile both kustomize and
the plugin. Tooling could be built to make Go
_plugin sharing_ easier, but this requires some
critical mass of _plugin authoring_, which in turn
is hampered by confusion around sharing.
[Go modules], once they are more widely adopted,
will solve one of the biggest plugin sharing
difficulties - ambiguous plugin vs host
dependencies.

View File

@@ -0,0 +1,229 @@
# Exec plugin on linux in 60 seconds
This is a (no reading) 60 second copy/paste guided
example. Full plugin docs [here](README.md).
This demo writes and uses a somewhat ridiculous
_exec_ plugin (written in bash) that generates a
`ConfigMap`.
This is a guide to try it without damaging your
current setup.
#### requirements
* linux, git, curl, Go 1.12
## Make a place to work
```
DEMO=$(mktemp -d)
```
## Create a kustomization
Make a kustomization directory to
hold all your config:
```
MYAPP=$DEMO/myapp
mkdir -p $MYAPP
```
Make a deployment config:
```
cat <<'EOF' >$MYAPP/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: the-deployment
spec:
replicas: 3
template:
spec:
containers:
- name: the-container
image: monopole/hello:1
command: ["/hello",
"--port=8080",
"--date=$(THE_DATE)",
"--enableRiskyFeature=$(ENABLE_RISKY)"]
ports:
- containerPort: 8080
env:
- name: THE_DATE
valueFrom:
configMapKeyRef:
name: the-map
key: today
- name: ALT_GREETING
valueFrom:
configMapKeyRef:
name: the-map
key: altGreeting
- name: ENABLE_RISKY
valueFrom:
configMapKeyRef:
name: the-map
key: enableRisky
EOF
```
Make a service config:
```
cat <<EOF >$MYAPP/service.yaml
kind: Service
apiVersion: v1
metadata:
name: the-service
spec:
type: LoadBalancer
ports:
- protocol: TCP
port: 8666
targetPort: 8080
EOF
```
Now make a config file for the plugin
you're about to write.
This config file is just another k8s resource
object. The values of its `apiVersion` and `kind`
fields are used to _find_ the plugin code on your
filesystem (more on this later).
```
cat <<'EOF' >$MYAPP/cmGenerator.yaml
apiVersion: myDevOpsTeam
kind: SillyConfigMapGenerator
metadata:
name: whatever
argsOneLiner: Bienvenue true
EOF
```
Finally, make a kustomization file
referencing all of the above:
```
cat <<EOF >$MYAPP/kustomization.yaml
commonLabels:
app: hello
resources:
- deployment.yaml
- service.yaml
generators:
- cmGenerator.yaml
EOF
```
Review the files
```
ls -C1 $MYAPP
```
## Make a home for plugins
Plugins must live in a particular place for
kustomize to find them.
This demo will use the ephemeral directory:
```
PLUGIN_ROOT=$DEMO/kustomize/plugin
```
The plugin config defined above in
`$MYAPP/cmGenerator.yaml` specifies:
> ```
> apiVersion: myDevOpsTeam
> kind: SillyConfigMapGenerator
> ```
This means the plugin must live in a directory
named:
```
MY_PLUGIN_DIR=$PLUGIN_ROOT/myDevOpsTeam/sillyconfigmapgenerator
mkdir -p $MY_PLUGIN_DIR
```
The directory name is the plugin config's
_apiVersion_ followed by its lower-cased _kind_.
A plugin gets its own directory to hold itself,
its tests and any supplemental data files it
might need.
## Create the plugin
There are two kinds of plugins, _exec_ and _Go_.
Make an _exec_ plugin, installing it to the
correct directory and file name. The file name
must match the plugin's _kind_ (in this case,
`SillyConfigMapGenerator`):
```
cat <<'EOF' >$MY_PLUGIN_DIR/SillyConfigMapGenerator
#!/bin/bash
# Skip the config file name argument.
shift
today=`date +%F`
echo "
kind: ConfigMap
apiVersion: v1
metadata:
name: the-map
data:
today: $today
altGreeting: "$1"
enableRisky: "$2"
"
EOF
```
By definition, an _exec_ plugin must be executable:
```
chmod a+x $MY_PLUGIN_DIR/SillyConfigMapGenerator
```
## Download kustomize 3.0.0
```
mkdir -p $DEMO/bin
gh=https://github.com/kubernetes-sigs/kustomize/releases/download
url=$gh/v3.0.0-pre/kustomize_3.0.0-pre_linux_amd64
curl -o $DEMO/bin/kustomize -L $url
chmod u+x $DEMO/bin/kustomize
```
## Review the layout
```
tree $DEMO
```
## Build your app, using the plugin:
```
XDG_CONFIG_HOME=$DEMO $DEMO/bin/kustomize build --enable_alpha_plugins $MYAPP
```
Above, if you had set
> ```
> PLUGIN_ROOT=$HOME/.config/kustomize/plugin
> ```
there would be no need to use `XDG_CONFIG_HOME` in the
_kustomize_ command above.

View File

@@ -0,0 +1,308 @@
# Go Plugin Guided Example for Linux
This is a (no reading) 60 second copy/paste guided
example. Full plugin docs [here](README.md).
[SopsEncodedSecrets repository]: https://github.com/monopole/sopsencodedsecrets
[Go plugin]: https://golang.org/pkg/plugin
This demo uses a Go plugin, `SopsEncodedSecrets`,
that lives in the [sopsencodedsecrets repository].
This is an inprocess [Go plugin], not an
sub-process exec plugin that happens to be written
in Go (which is another option for Go authors).
This is a guide to try it without damaging your
current setup.
#### requirements
* linux, git, curl, Go 1.12
* Google cloud (gcloud) install
* a Google account (will use Google kms -
volunteers needed to convert to a GPG example).
## Make a place to work
```
DEMO=$(mktemp -d)
```
## Install kustomize
Need v3.0.0 for what follows:
```
GOBIN=$DEMO/bin go get sigs.k8s.io/kustomize/v3/cmd/kustomize@v3.0.0-pre
```
## Make a home for plugins
A kustomize plugin is fully determined by
its configuration file and source code.
[required fields]: https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/#required-fields
Kustomize plugin configuration files are formatted
as kubernetes resource objects, meaning
`apiVersion`, `kind` and `metadata` are [required
fields] in these config files.
The kustomize program reads the config file
(because the config file name appears in the
`generators` or `transformers` field in the
kustomization file), then locates the Go plugin's
object code at the following location:
> ```
> $XGD_CONFIG_HOME/kustomize/plugin/$apiVersion/$lKind/$kind.so
> ```
where `lKind` holds the lowercased kind. The
plugin is then loaded and fed its config, and the
plugin's output becomes part of the overall
`kustomize build` process.
The same plugin might be used multiple times in
one kustomize build, but with different config
files. Also, kustomize might customize config
data before sending it to the plugin, for whatever
reason. For these reasons, kustomize owns the
mapping between plugins and config data; it's not
left to plugins to find their own config.
This demo will house the plugin it uses at the
ephemeral directory
```
PLUGIN_ROOT=$DEMO/kustomize/plugin
```
and ephemerally set `XGD_CONFIG_HOME` on a command
line below.
### What apiVersion and kind?
At this stage in the development of kustomize
plugins, plugin code doesn't know or care what
`apiVersion` or `kind` appears in the config file
sent to it.
The plugin could check these fields, but it's the
remaining fields that provide actual configuration
data, and at this point the successful parsing of
these other fields are the only thing that matters
to a plugin.
This demo uses a plugin called _SopsEncodedSecrets_,
and it lives in the [SopsEncodedSecrets repository].
Somewhat arbitrarily, we'll chose to install
this plugin with
```
apiVersion=mygenerators
kind=SopsEncodedSecrets
```
### Define the plugin's home dir
By convention, the ultimate home of the plugin
code and supplemental data, tests, documentation,
etc. is the lowercase form of its kind.
```
lKind=$(echo $kind | awk '{print tolower($0)}')
```
### Download the SopsEncodedSecrets plugin
In this case, the repo name matches the lowercase
kind already, so we just clone the repo and get
the proper directory name automatically:
```
mkdir -p $PLUGIN_ROOT/${apiVersion}
cd $PLUGIN_ROOT/${apiVersion}
git clone git@github.com:monopole/sopsencodedsecrets.git
```
Remember this directory:
```
MY_PLUGIN_DIR=$PLUGIN_ROOT/${apiVersion}/${lKind}
```
### Try the plugin's own test
Plugins may come with their own tests.
This one does, and it hopefully passes:
```
cd $MY_PLUGIN_DIR
go test SopsEncodedSecrets_test.go
```
Build the object code for use by kustomize:
```
cd $MY_PLUGIN_DIR
go build -buildmode plugin -o ${kind}.so ${kind}.go
```
This step may succeed, but kustomize might
ultimately fail to load the plugin because of
dependency [skew].
[skew]: https://github.com/kubernetes-sigs/kustomize/blob/master/docs/plugins.md#caveats
[used in this demo]: #install-kustomize
On load failure
* be sure to build the plugin with the same
version of Go (`go1.12`) on the same `$GOOS`
(`linux`) and `$GOARCH` (`amd64`) used to build
the kustomize being [used in this demo].
* change the plugin's dependencies in `go.mod` to
match those used to build kustomize (check the
`go.mod` used in the tagged commit).
Lacking tools and metadata to allow this to be
automated, there won't be a Go plugin ecosystem.
Kustomize has adopted a Go plugin architecture as
to ease accept new generators and transformers
(just write a plugin), and to be sure that native
operations (also constructed and tested as
plugins) are compartmentalized, orderable and
reusable instead of bizarrely woven throughout the
code as a individual special cases.
## Create a kustomization
Make a kustomization directory to
hold all your config:
```
MYAPP=$DEMO/myapp
mkdir -p $MYAPP
```
Make a config file for the SopsEncodedSecrets plugin.
Its `apiVersion` and `kind` allow the plugin to be
found:
```
cat <<EOF >$MYAPP/secGenerator.yaml
apiVersion: ${apiVersion}
kind: ${kind}
metadata:
name: forbiddenValues
namespace: production
file: myEncryptedData.yaml
keys:
- ROCKET
- CAR
EOF
```
This plugin expects to find more data in
`myEncryptedData.yaml`; we'll get to that shortly.
Make a kustomization file referencing the plugin
config:
```
cat <<EOF >$MYAPP/kustomization.yaml
commonLabels:
app: hello
generators:
- secGenerator.yaml
EOF
```
Now for the hard part. Generate the real encrypted data.
### Assure you have a Google Cloud sops key ring.
We're going to use [sops](https://github.com/mozilla/sops) to encode a file.
Try this:
```
gcloud kms keys list --location global --keyring sops
```
If it succeeds, presumably you've already
created keys and placed them in a keyring called `sops`.
If not, do this:
```
gcloud kms keyrings create sops --location global
gcloud kms keys create sops-key --location global \
--keyring sops --purpose encryption
```
Extract your keyLocation for use below:
```
keyLocation=$(\
gcloud kms keys list --location global --keyring sops |\
grep GOOGLE | cut -d " " -f1)
echo $keyLocation
```
### Install `sops`
```
GOBIN=$DEMO/bin go install go.mozilla.org/sops/cmd/sops
```
### Create data encrypted with your Google Cloud key
Create raw data to encrypt:
```
cat <<EOF >$MYAPP/myClearData.yaml
VEGETABLE: carrot
ROCKET: saturn-v
FRUIT: apple
CAR: dymaxion
EOF
```
Encrypt the data into file the plugin wants to read:
```
$DEMO/bin/sops --encrypt \
--gcp-kms $keyLocation \
$MYAPP/myClearData.yaml >$MYAPP/myEncryptedData.yaml
```
Review the files
```
tree $DEMO
```
## Build your app, using the plugin:
```
XDG_CONFIG_HOME=$DEMO $DEMO/bin/kustomize build --enable_alpha_plugins $MYAPP
```
This should emit a kubernetes secret, with
encrypted data for the names `ROCKET` and `CAR`.
Above, if you had set
> ```
> PLUGIN_ROOT=$HOME/.config/kustomize/plugin
> ```
there would be no need to use `XDG_CONFIG_HOME` in the
_kustomize_ command above.