Skip to content

Commit d1ba857

Browse files
authored
Merge pull request #14 from fluxcd/generate
Generate kustomization.yaml automatically
2 parents 0c86ccf + 73e09ac commit d1ba857

File tree

14 files changed

+227
-14
lines changed

14 files changed

+227
-14
lines changed

.github/workflows/e2e.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ jobs:
5454
- name: Run smoke tests
5555
run: |
5656
kubectl apply -k ./config/testdata/webapp
57+
kubectl apply -k ./config/testdata/generate
5758
kubectl wait kustomizations/frontend --for=condition=ready --timeout=4m
59+
kubectl wait kustomizations/generate --for=condition=ready --timeout=4m
5860
kubectl -n kustomize-system logs deploy/kustomize-controller
5961
- name: Debug failure
6062
if: failure()

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77

88
The kustomize-controller is a continuous delivery tool for Kubernetes, specialized in running
99
CD pipelines inside the cluster for workloads and infrastructure manifests
10-
generated with Kustomize coming from source control systems.
10+
coming from source control systems.
1111

12-
![overview](docs/diagrams/fluxcd-kustomize-source-controllers.png)
12+
![overview](docs/diagrams/kustomize-controller-overview.png)
1313

1414
Features:
1515
* watches for [Kustomization](docs/spec/v1alpha1/README.md) objects
1616
* fetches artifacts produced by [source-controller](https://github.com/fluxcd/source-controller) from `Source` objects
1717
* watches `Source` objects for revision changes
18+
* generates the `kustomization.yaml` file if needed
1819
* generates Kubernetes manifests with kustomize build
1920
* validates the build output with client-side or APIServer dry-run
2021
* applies the generated manifests on the cluster
@@ -102,17 +103,22 @@ spec:
102103
timeout: 80s
103104
```
104105
106+
> **Note** that if your repository contains only plain Kubernetes manifests,
107+
> you can configure the controller to
108+
> [automatically generate](docs/spec/v1alpha1/kustomization.md#generate-kustomizationyaml)
109+
> a kustomization.yaml file inside the specified path.
110+
105111
A detailed explanation of the Kustomization object and its fields
106112
can be found in the [specification doc](docs/spec/v1alpha1/README.md).
107113
108-
![pipeline](docs/diagrams/fluxcd-kustomization-pipeline.png)
109-
110114
Based on the above definition, the kustomize-controller fetches the Git repository content from source-controller,
111115
generates Kubernetes manifests by running kustomize build inside `./overlays/dev/`,
112116
and validates them with a dry-run apply. If the manifests pass validation, the controller will apply them
113117
on the cluster and starts the health assessment of the deployed workload. If the health checks are passing, the
114118
Kustomization object status transitions to a ready state.
115119

120+
![workflow](docs/diagrams/kustomize-controller-flow.png)
121+
116122
You can wait for the kustomize controller to complete the deployment with:
117123

118124
```bash
@@ -250,6 +256,8 @@ set in the Git repository manifest.
250256

251257
The kustomize controller can post message to Slack or Discord whenever a kustomization status changes.
252258

259+
![pipeline](docs/diagrams/kustomize-controller-pipeline.png)
260+
253261
Alerting can be configured by creating a profile that targets a list of kustomizations:
254262

255263
```yaml

api/v1alpha1/kustomization_types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ type KustomizationSpec struct {
3030
// +optional
3131
DependsOn []string `json:"dependsOn,omitempty"`
3232

33+
// When enabled, the kustomization.yaml is automatically generated
34+
// for all the Kubernetes manifests in the specified path and sub-directories.
35+
// The generated kustomization.yaml contains a label transformer matching the prune field.
36+
// +optional
37+
Generate bool `json:"generate,omitempty"`
38+
3339
// The interval at which to apply the kustomization.
3440
// +required
3541
Interval metav1.Duration `json:"interval"`

config/crd/bases/kustomize.fluxcd.io_kustomizations.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ spec:
5252
items:
5353
type: string
5454
type: array
55+
generate:
56+
description: When enabled, the kustomization.yaml is automatically generated
57+
for all the Kubernetes manifests in the specified path and sub-directories.
58+
The generated kustomization.yaml contains a label transformer matching
59+
the prune field.
60+
type: boolean
5561
healthChecks:
5662
description: A list of workloads (Deployments, DaemonSets and StatefulSets)
5763
to be included in the health assessment.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
apiVersion: kustomize.fluxcd.io/v1alpha1
2+
kind: Kustomization
3+
metadata:
4+
name: generate
5+
spec:
6+
interval: 5m
7+
generate: true
8+
path: "./plain/"
9+
prune: "env=test"
10+
sourceRef:
11+
kind: GitRepository
12+
name: webapp
13+
healthChecks:
14+
- kind: Deployment
15+
name: backend
16+
namespace: test
17+
- kind: Deployment
18+
name: frontend
19+
namespace: test
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: source.fluxcd.io/v1alpha1
2+
kind: GitRepository
3+
metadata:
4+
name: webapp
5+
spec:
6+
interval: 10m
7+
url: https://github.com/stefanprodan/podinfo-deploy
8+
ref:
9+
branch: master
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
apiVersion: kustomize.config.k8s.io/v1beta1
2+
kind: Kustomization
3+
resources:
4+
- gitrepository.yaml
5+
- generate.yaml
6+

controllers/kustomization_controller.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
package controllers
1818

1919
import (
20+
"bufio"
21+
"bytes"
2022
"context"
2123
"errors"
2224
"fmt"
@@ -25,6 +27,7 @@ import (
2527
"os/exec"
2628
"path"
2729
"strings"
30+
"text/template"
2831
"time"
2932

3033
"github.com/go-logr/logr"
@@ -198,6 +201,16 @@ func (r *KustomizationReconciler) sync(
198201
), err
199202
}
200203

204+
// generate kustomization.yaml
205+
err = r.generate(kustomization, tmpDir)
206+
if err != nil {
207+
return kustomizev1.KustomizationNotReady(
208+
kustomization,
209+
kustomizev1.BuildFailedReason,
210+
"kustomize create failed",
211+
), err
212+
}
213+
201214
// kustomize build
202215
err = r.build(kustomization, tmpDir)
203216
if err != nil {
@@ -263,6 +276,108 @@ func (r *KustomizationReconciler) download(kustomization kustomizev1.Kustomizati
263276
return nil
264277
}
265278

279+
func (r *KustomizationReconciler) generate(kustomization kustomizev1.Kustomization, tmpDir string) error {
280+
if !kustomization.Spec.Generate {
281+
return nil
282+
}
283+
284+
timeout := kustomization.GetTimeout() + (time.Second * 1)
285+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
286+
defer cancel()
287+
288+
dirPath := path.Join(tmpDir, kustomization.Spec.Path)
289+
cmd := fmt.Sprintf("cd %s && kustomize create --autodetect --recursive", dirPath)
290+
command := exec.CommandContext(ctx, "/bin/sh", "-c", cmd)
291+
output, err := command.CombinedOutput()
292+
if err != nil {
293+
if errors.Is(err, context.DeadlineExceeded) {
294+
return err
295+
}
296+
return fmt.Errorf("kustomize create failed: %s", string(output))
297+
}
298+
299+
if err := r.generateLabelTransformer(kustomization, dirPath); err != nil {
300+
return err
301+
}
302+
303+
return nil
304+
}
305+
306+
func (r *KustomizationReconciler) generateLabelTransformer(kustomization kustomizev1.Kustomization, dirPath string) error {
307+
labelTransformer := `
308+
apiVersion: builtin
309+
kind: LabelTransformer
310+
metadata:
311+
name: labels
312+
labels:
313+
{{- range $key, $value := . }}
314+
{{ $key }}: {{ $value }}
315+
{{- end }}
316+
fieldSpecs:
317+
- path: metadata/labels
318+
create: true
319+
`
320+
321+
transformers := `
322+
transformers:
323+
- gc-labels.yaml
324+
`
325+
326+
prune := kustomization.Spec.Prune
327+
if prune == "" {
328+
return nil
329+
}
330+
331+
// transform prune into label selectors
332+
selectors := make(map[string]string)
333+
for _, ls := range strings.Split(prune, ",") {
334+
if kv := strings.Split(ls, "="); len(kv) == 2 {
335+
selectors[kv[0]] = kv[1]
336+
}
337+
}
338+
339+
t, err := template.New("tmpl").Parse(labelTransformer)
340+
if err != nil {
341+
return fmt.Errorf("labelTransformer template parsing failed: %w", err)
342+
}
343+
344+
var data bytes.Buffer
345+
writer := bufio.NewWriter(&data)
346+
if err := t.Execute(writer, selectors); err != nil {
347+
return fmt.Errorf("labelTransformer template excution failed: %w", err)
348+
}
349+
350+
if err := writer.Flush(); err != nil {
351+
return fmt.Errorf("labelTransformer flush failed: %w", err)
352+
}
353+
354+
file, err := os.Create(path.Join(dirPath, "gc-labels.yaml"))
355+
if err != nil {
356+
return fmt.Errorf("labelTransformer create failed: %w", err)
357+
}
358+
defer file.Close()
359+
360+
if _, err := file.WriteString(data.String()); err != nil {
361+
return fmt.Errorf("labelTransformer write failed: %w", err)
362+
}
363+
364+
if err := file.Sync(); err != nil {
365+
return fmt.Errorf("labelTransformer sync failed: %w", err)
366+
}
367+
368+
kfile, err := os.OpenFile(path.Join(dirPath, "kustomization.yaml"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
369+
if err != nil {
370+
return fmt.Errorf("kustomization file open failed: %w", err)
371+
}
372+
defer kfile.Close()
373+
374+
if _, err := kfile.WriteString(transformers); err != nil {
375+
return fmt.Errorf("kustomization file append failed: %w", err)
376+
}
377+
378+
return nil
379+
}
380+
266381
func (r *KustomizationReconciler) build(kustomization kustomizev1.Kustomization, tmpDir string) error {
267382
timeout := kustomization.GetTimeout() + (time.Second * 1)
268383
ctx, cancel := context.WithTimeout(context.Background(), timeout)

controllers/kustomization_predicate.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"context"
2121
"fmt"
2222
"os/exec"
23-
"strings"
2423

2524
"github.com/go-logr/logr"
2625
"sigs.k8s.io/controller-runtime/pkg/event"
@@ -76,12 +75,12 @@ func (gc KustomizationGarbageCollectPredicate) Delete(e event.DeleteEvent) bool
7675
command := exec.CommandContext(ctx, "/bin/sh", "-c", cmd)
7776
if output, err := command.CombinedOutput(); err != nil {
7877
gc.Log.Error(err, "Garbage collection failed",
79-
"output", string(output),
80-
strings.ToLower(k.Kind), fmt.Sprintf("%s/%s", k.GetNamespace(), k.GetName()))
78+
"kustomization", fmt.Sprintf("%s/%s", k.GetNamespace(), k.GetName()),
79+
"output", string(output))
8180
} else {
8281
gc.Log.Info("Garbage collection completed",
83-
"output", string(output),
84-
strings.ToLower(k.Kind), fmt.Sprintf("%s/%s", k.GetNamespace(), k.GetName()))
82+
"kustomization", fmt.Sprintf("%s/%s", k.GetNamespace(), k.GetName()),
83+
"output", string(output))
8584
}
8685
}
8786
}

0 commit comments

Comments
 (0)