Skip to content

Commit 8c0e186

Browse files
authored
Fix and cleanup Kpt fn integration (#5886)
* Fix and cleanup Kpt fn integration * Makfile to respect GOBIN * Fixing tests and comments * Checking kpt version < 1.0.0
1 parent cbeefdc commit 8c0e186

File tree

3 files changed

+209
-248
lines changed

3 files changed

+209
-248
lines changed

Makefile

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
GOPATH ?= $(shell go env GOPATH)
15+
GOBIN ?= $(or $(shell go env GOBIN),$(GOPATH)/bin)
1516
GOOS ?= $(shell go env GOOS)
1617
GOARCH ?= $(shell go env GOARCH)
1718
BUILD_DIR ?= ./out
@@ -73,7 +74,7 @@ $(BUILD_DIR)/$(PROJECT): $(STATIK_FILES) $(GO_FILES) $(BUILD_DIR)
7374
.PHONY: install
7475
install: $(BUILD_DIR)/$(PROJECT)
7576
mkdir -p $(GOPATH)/bin
76-
cp $(BUILD_DIR)/$(PROJECT) $(GOPATH)/bin/$(PROJECT)
77+
cp $(BUILD_DIR)/$(PROJECT) $(GOBIN)/$(PROJECT)
7778

7879
.PRECIOUS: $(foreach platform, $(SUPPORTED_PLATFORMS), $(BUILD_DIR)/$(PROJECT)-$(platform))
7980

pkg/skaffold/deploy/kpt/kpt.go

+89-164
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import (
3030

3131
"golang.org/x/mod/semver"
3232
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
33-
"sigs.k8s.io/kustomize/kyaml/fn/framework"
3433
k8syaml "sigs.k8s.io/yaml"
3534

3635
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
@@ -52,8 +51,9 @@ const (
5251
kptFnAnnotation = "config.kubernetes.io/function"
5352
kptFnLocalConfig = "config.kubernetes.io/local-config"
5453

55-
kptDownloadLink = "https://googlecontainertools.github.io/kpt/installation/"
56-
kptMinVersion = "0.34.0"
54+
kptDownloadLink = "https://googlecontainertools.github.io/kpt/installation/"
55+
kptMinVersionInclusive = "v0.38.1"
56+
kptMaxVersionExclusive = "v1.0.0"
5757

5858
kustomizeDownloadLink = "https://kubernetes-sigs.github.io/kustomize/installation/"
5959
kustomizeMinVersion = "v3.2.3"
@@ -67,6 +67,7 @@ type Deployer struct {
6767
insecureRegistries map[string]bool
6868
labels map[string]string
6969
globalConfig string
70+
hasKustomization func(string) bool
7071
kubeContext string
7172
kubeConfig string
7273
namespace string
@@ -83,6 +84,7 @@ func NewDeployer(cfg Config, labels map[string]string, d *latestV1.KptDeploy) *D
8384
insecureRegistries: cfg.GetInsecureRegistries(),
8485
labels: labels,
8586
globalConfig: cfg.GlobalConfig(),
87+
hasKustomization: hasKustomization,
8688
kubeContext: cfg.GetKubeContext(),
8789
kubeConfig: cfg.GetKubeConfig(),
8890
namespace: cfg.GetKubeNamespace(),
@@ -99,22 +101,23 @@ func versionCheck(dir string, stdout io.Writer) error {
99101
return fmt.Errorf("kpt is not installed yet\nSee kpt installation: %v",
100102
kptDownloadLink)
101103
}
102-
version := strings.TrimSuffix(string(out), "\n")
103104
// kpt follows semver but does not have "v" prefix.
104-
if !semver.IsValid("v" + version) {
105-
return fmt.Errorf("unknown kpt version %v\nPlease upgrade your "+
106-
"local kpt CLI to a version >= %v\nSee kpt installation: %v",
107-
string(out), kptMinVersion, kptDownloadLink)
105+
version := "v" + strings.TrimSuffix(string(out), "\n")
106+
if !semver.IsValid(version) {
107+
return fmt.Errorf("unknown kpt version %v\nPlease install "+
108+
"kpt %v <= version < %v\nSee kpt installation: %v",
109+
string(out), kptMinVersionInclusive, kptMaxVersionExclusive, kptDownloadLink)
108110
}
109-
if semver.Compare("v"+version, "v"+kptMinVersion) < 0 {
110-
return fmt.Errorf("you are using kpt %q\nPlease update your kpt version to"+
111-
" >= %v\nSee kpt installation: %v", version[0], kptMinVersion, kptDownloadLink)
111+
if semver.Compare(version, kptMinVersionInclusive) < 0 ||
112+
semver.Compare(version, kptMaxVersionExclusive) >= 0 {
113+
return fmt.Errorf("you are using kpt %q\nPlease install "+
114+
"kpt %v <= version < %v\nSee kpt installation: %v",
115+
version, kptMinVersionInclusive, kptMaxVersionExclusive, kptDownloadLink)
112116
}
113117

114118
// Users can choose not to use kustomize in kpt deployer mode. We only check the kustomize
115119
// version when kustomization.yaml config is directed under .deploy.kpt.dir path.
116-
_, err = kustomize.FindKustomizationConfig(dir)
117-
if err == nil {
120+
if hasKustomization(dir) {
118121
kustomizeCmd := exec.Command("kustomize", "version")
119122
out, err := util.RunCmdOut(kustomizeCmd)
120123
if err != nil {
@@ -154,16 +157,8 @@ func (k *Deployer) Deploy(ctx context.Context, out io.Writer, builds []graph.Art
154157
}
155158
endTrace()
156159

157-
_, endTrace = instrumentation.StartTrace(ctx, "Deploy_getKptFnRunArgs")
158-
flags, err := k.getKptFnRunArgs()
159-
if err != nil {
160-
endTrace(instrumentation.TraceEndError(err))
161-
return []string{}, err
162-
}
163-
endTrace()
164-
165160
childCtx, endTrace := instrumentation.StartTrace(ctx, "Deploy_renderManifests")
166-
manifests, err := k.renderManifests(childCtx, out, builds, flags)
161+
manifests, err := k.renderManifests(childCtx, builds)
167162
if err != nil {
168163
endTrace(instrumentation.TraceEndError(err))
169164
return nil, err
@@ -191,7 +186,9 @@ func (k *Deployer) Deploy(ctx context.Context, out io.Writer, builds []graph.Art
191186
endTrace()
192187

193188
_, endTrace = instrumentation.StartTrace(ctx, "Deploy_manifest.Write")
194-
manifest.Write(manifests.String(), filepath.Join(applyDir, "resources.yaml"), out)
189+
if err = sink(ctx, []byte(manifests.String()), applyDir); err != nil {
190+
return nil, err
191+
}
195192
endTrace()
196193

197194
childCtx, endTrace = instrumentation.StartTrace(ctx, "Deploy_execKptCommand")
@@ -278,18 +275,9 @@ func (k *Deployer) Render(ctx context.Context, out io.Writer, builds []graph.Art
278275
endTrace(instrumentation.TraceEndError(err))
279276
return err
280277
}
281-
endTrace()
282-
283-
_, endTrace = instrumentation.StartTrace(ctx, "Render_getKptFnRunArgs")
284-
flags, err := k.getKptFnRunArgs()
285-
if err != nil {
286-
endTrace(instrumentation.TraceEndError(err))
287-
return err
288-
}
289-
endTrace()
290278

291279
childCtx, endTrace := instrumentation.StartTrace(ctx, "Render_renderManifests")
292-
manifests, err := k.renderManifests(childCtx, out, builds, flags)
280+
manifests, err := k.renderManifests(childCtx, builds)
293281
if err != nil {
294282
endTrace(instrumentation.TraceEndError(err))
295283
return err
@@ -304,9 +292,13 @@ func (k *Deployer) Render(ctx context.Context, out io.Writer, builds []graph.Art
304292
// renderManifests handles a majority of the hydration process for manifests.
305293
// This involves reading configs from a source directory, running kustomize build, running kpt pipelines,
306294
// adding image digests, and adding run-id labels.
307-
func (k *Deployer) renderManifests(ctx context.Context, _ io.Writer, builds []graph.Artifact,
308-
flags []string) (manifest.ManifestList, error) {
309-
var err error
295+
func (k *Deployer) renderManifests(ctx context.Context, builds []graph.Artifact) (
296+
manifest.ManifestList, error) {
297+
flags, err := k.getKptFnRunArgs()
298+
if err != nil {
299+
return nil, err
300+
}
301+
310302
debugHelpersRegistry, err := config.GetDebugHelpersRegistry(k.globalConfig)
311303
if err != nil {
312304
return nil, fmt.Errorf("retrieving debug helpers registry: %w", err)
@@ -317,112 +309,64 @@ func (k *Deployer) renderManifests(ctx context.Context, _ io.Writer, builds []gr
317309
cmd := exec.CommandContext(
318310
ctx, "kpt", kptCommandArgs(k.Dir, []string{"fn", "source"},
319311
nil, nil)...)
320-
buf, err = util.RunCmdOut(cmd)
321-
if err != nil {
312+
if buf, err = util.RunCmdOut(cmd); err != nil {
322313
return nil, fmt.Errorf("reading config manifests: %w", err)
323314
}
324315

325-
// A workaround for issue https://github.com/GoogleContainerTools/kpt/issues/1149
326-
// Problem: fn-path cannot be recognized in kpt pipeline mode, and it results in that
327-
// the kpt functions in are ignored.
328-
// Solution: pull kpt functions specifically from the kpt source inputs (getKptFunc) and
329-
// adds it back to the pipeline after kustomize build finishes (append kptFn).
330-
var kptFnBuf []byte
331-
if len(k.Fn.FnPath) > 0 {
332-
cmd = exec.CommandContext(
333-
ctx, "kpt", kptCommandArgs(k.Fn.FnPath, []string{"fn", "source"},
334-
nil, nil)...)
335-
if kptFnBuf, err = util.RunCmdOut(cmd); err != nil {
336-
return nil, fmt.Errorf("kpt source the fn-path config %v", err)
337-
}
338-
} else {
339-
kptFnBuf = buf
340-
}
341-
kptFn, err := k.getKptFunc(kptFnBuf)
342-
if err != nil {
343-
return nil, err
316+
// Run kpt functions against the manifests read from source.
317+
cmd = exec.CommandContext(ctx, "kpt", kptCommandArgs("", []string{"fn", "run"}, flags, nil)...)
318+
cmd.Stdin = bytes.NewBuffer(buf)
319+
if buf, err = util.RunCmdOut(cmd); err != nil {
320+
return nil, fmt.Errorf("running kpt functions: %w", err)
344321
}
345322

346-
// Hydrate the manifests source.
347-
// Note: kustomize cannot be used as a kpt fn yet and thus we add a kustomize cmd step in the kpt pipeline:
348-
// kpt source --> (workaround) kustomize build --> kpt run --> kpt sink.
349-
_, err = kustomize.FindKustomizationConfig(k.Dir)
350-
// Only run kustomize if kustomization.yaml is found.
351-
if err == nil {
352-
// Note: A tmp dir is used to provide kustomize the manifest directory.
353-
// Once the unified kpt/kustomize is done, kustomize can be run as a kpt fn step and
354-
// this additional directory creation/deletion will no longer be needed.
355-
if err := os.RemoveAll(tmpKustomizeDir); err != nil {
356-
return nil, fmt.Errorf("removing %v:%w", tmpKustomizeDir, err)
357-
}
358-
if err := os.MkdirAll(tmpKustomizeDir, os.ModePerm); err != nil {
359-
return nil, err
360-
}
361-
defer func() {
362-
os.RemoveAll(tmpKustomizeDir)
363-
}()
364-
365-
err = k.sink(ctx, buf, tmpKustomizeDir)
366-
if err != nil {
367-
return nil, err
323+
// Run kustomize on the output from the kpt functions if a kustomization is found.
324+
// Note: kustomize cannot be used as a kpt fn yet and thus we run kustomize in a temp dir
325+
// in the kpt pipeline:
326+
// kpt source --> kpt run --> (workaround if kustomization exists) kustomize build --> kpt sink.
327+
//
328+
// Note: Optimally the user would be able to control the order in which kpt functions and
329+
// Kustomize build happens, and even have Kustomize build happen between Kpt fn invocations.
330+
// However, since we currently don't expose an API supporting that level of control running
331+
// Kustomize build last seems like the best option.
332+
// Pros:
333+
// - Kustomize will remove all comments which breaks any Kpt fn relying on YAML comments. This
334+
// includes https://github.com/GoogleContainerTools/kpt-functions-catalog/tree/master/functions/go/apply-setters
335+
// which is the upcoming replacement for kpt cfg set and it will likely receive wide usage.
336+
// This is the main reason for Kustomize last approach winning.
337+
// - Kustomize mangles the directory structure so any Kpt fn relying on th relative file
338+
// location of a resource as described by the config.kubernetes.io/path annotation will break
339+
// if run after Kustomize.
340+
// - This allows Kpt fns to modify and even create Kustomizations.
341+
// Cons:
342+
// - Kpt fns that expects the output of kustomize build as input might not work as expected,
343+
// especially if the Kustomization references resources outside of the kpt dir.
344+
// - Kpt fn run chokes on JSON patch files because the root node is an array. This can be worked
345+
// around by avoiding the file extensions kpt fn reads from for such files (.yaml, .yml and
346+
// .json) or inlining the patch.
347+
defer func() {
348+
if err = os.RemoveAll(tmpKustomizeDir); err != nil {
349+
fmt.Printf("Unable to delete temporary Kusomize directory: %v\n", err)
368350
}
351+
}()
352+
if err = sink(ctx, buf, tmpKustomizeDir); err != nil {
353+
return nil, err
354+
}
369355

370-
cmd := exec.CommandContext(ctx, "kustomize", append([]string{"build"}, tmpKustomizeDir)...)
371-
buf, err = util.RunCmdOut(cmd)
372-
if err != nil {
356+
// Only run kustomize if kustomization.yaml is found in the output from the kpt functions.
357+
if k.hasKustomization(tmpKustomizeDir) {
358+
cmd = exec.CommandContext(ctx, "kustomize", append([]string{"build"}, tmpKustomizeDir)...)
359+
if buf, err = util.RunCmdOut(cmd); err != nil {
373360
return nil, fmt.Errorf("kustomize build: %w", err)
374361
}
375-
} else {
376-
// Note: Here we use kpt ResourceList as the media to store the config source.
377-
// Eventually, skaffold should not need to construct a kpt inner resource but only use kpt commands and
378-
// the new Kptfile(v1) to establish a hydration pipeline.
379-
input := bytes.NewBufferString(string(buf))
380-
rl := framework.ResourceList{
381-
Reader: input,
382-
}
383-
// Manipulate the kustomize "Rnode"(Kustomize term) and pulls out the "Items"
384-
// from ResourceLists.
385-
if err := rl.Read(); err != nil {
386-
return nil, fmt.Errorf("reading ResourceList %w", err)
387-
}
388-
389-
var newBuf []byte
390-
for i := range rl.Items {
391-
item, err := rl.Items[i].String()
392-
if err != nil {
393-
return nil, fmt.Errorf("reading Item %w", err)
394-
}
395-
newBuf = append(newBuf, []byte(item)...)
396-
}
397-
buf = newBuf
398-
}
399-
400-
// Run kpt functions against the hydrated manifests.
401-
cmd = exec.CommandContext(ctx, "kpt", kptCommandArgs("", []string{"fn", "run"}, flags, nil)...)
402-
buf = append(buf, []byte("---\n")...)
403-
buf = append(buf, kptFn...)
404-
cmd.Stdin = bytes.NewBuffer(buf)
405-
buf, err = util.RunCmdOut(cmd)
406-
if err != nil {
407-
return nil, fmt.Errorf("running kpt functions: %w", err)
408362
}
409363

410364
// Store the manipulated manifests to the sink dir.
411365
if k.Fn.SinkDir != "" {
412-
if err := os.RemoveAll(k.Fn.SinkDir); err != nil {
413-
return nil, fmt.Errorf("deleting sink directory %s: %w", k.Fn.SinkDir, err)
414-
}
415-
416-
if err := os.MkdirAll(k.Fn.SinkDir, os.ModePerm); err != nil {
417-
return nil, fmt.Errorf("creating sink directory %s: %w", k.Fn.SinkDir, err)
418-
}
419-
err = k.sink(ctx, buf, k.Fn.SinkDir)
420-
if err != nil {
421-
return nil, fmt.Errorf("sinking to directory %s: %w", k.Fn.SinkDir, err)
366+
if err = sink(ctx, buf, k.Fn.SinkDir); err != nil {
367+
return nil, err
422368
}
423-
fmt.Printf(
424-
"Manipulated resources are flattended and stored in your sink directory: %v\n",
425-
k.Fn.SinkDir)
369+
fmt.Printf("Manipulated resources are stored in your sink directory: %v\n", k.Fn.SinkDir)
426370
}
427371

428372
var manifests manifest.ManifestList
@@ -445,43 +389,21 @@ func (k *Deployer) renderManifests(ctx context.Context, _ io.Writer, builds []gr
445389
return manifests.SetLabels(k.labels)
446390
}
447391

448-
func (k *Deployer) getKptFunc(buf []byte) ([]byte, error) {
449-
input := bytes.NewBufferString(string(buf))
450-
rl := framework.ResourceList{
451-
Reader: input,
392+
func sink(ctx context.Context, buf []byte, sinkDir string) error {
393+
if err := os.RemoveAll(sinkDir); err != nil {
394+
return fmt.Errorf("deleting sink directory %s: %w", sinkDir, err)
452395
}
453-
// Manipulate the kustomize "Rnode"(Kustomize term) and pulls out the "Items"
454-
// from ResourceLists.
455-
if err := rl.Read(); err != nil {
456-
return nil, fmt.Errorf("reading ResourceList %w", err)
457-
}
458-
var kptFn []byte
459-
for i := range rl.Items {
460-
item, err := rl.Items[i].String()
461-
if err != nil {
462-
return nil, fmt.Errorf("reading Item %w", err)
463-
}
464-
var obj unstructured.Unstructured
465-
jByte, err := k8syaml.YAMLToJSON([]byte(item))
466-
if err != nil {
467-
continue
468-
}
469-
if err := obj.UnmarshalJSON(jByte); err != nil {
470-
continue
471-
}
472-
// Found kpt fn.
473-
if _, ok := obj.GetAnnotations()[kptFnAnnotation]; ok {
474-
kptFn = append(kptFn, []byte(item)...)
475-
}
396+
397+
if err := os.MkdirAll(sinkDir, os.ModePerm); err != nil {
398+
return fmt.Errorf("creating sink directory %s: %w", sinkDir, err)
476399
}
477-
return kptFn, nil
478-
}
479400

480-
func (k *Deployer) sink(ctx context.Context, buf []byte, sinkDir string) error {
481401
cmd := exec.CommandContext(ctx, "kpt", kptCommandArgs(sinkDir, []string{"fn", "sink"}, nil, nil)...)
482402
cmd.Stdin = bytes.NewBuffer(buf)
483-
_, err := util.RunCmdOut(cmd)
484-
return err
403+
if _, err := util.RunCmdOut(cmd); err != nil {
404+
return fmt.Errorf("sinking to directory %s: %w", sinkDir, err)
405+
}
406+
return nil
485407
}
486408

487409
// excludeKptFn adds an annotation "config.kubernetes.io/local-config: 'true'" to kpt function.
@@ -606,9 +528,7 @@ func getResources(root string) ([]string, error) {
606528

607529
// getKptFnRunArgs returns a list of arguments that the user specified for the `kpt fn run` command.
608530
func (k *Deployer) getKptFnRunArgs() ([]string, error) {
609-
// --dry-run sets the pipeline's output to STDOUT, otherwise output is set to sinkDir.
610-
// For now, k.Dir will be treated as sinkDir (and sourceDir).
611-
flags := []string{"--dry-run"}
531+
var flags []string
612532

613533
if k.Fn.GlobalScope {
614534
flags = append(flags, "--global-scope")
@@ -627,8 +547,8 @@ func (k *Deployer) getKptFnRunArgs() ([]string, error) {
627547
}
628548

629549
count := 0
630-
// fn-path is not supported due to kpt issue https://github.com/GoogleContainerTools/kpt/issues/1149
631550
if len(k.Fn.FnPath) > 0 {
551+
flags = append(flags, "--fn-path", k.Fn.FnPath)
632552
count++
633553
}
634554

@@ -697,3 +617,8 @@ func (k *Deployer) getGlobalFlags() []string {
697617

698618
return flags
699619
}
620+
621+
func hasKustomization(dir string) bool {
622+
_, err := kustomize.FindKustomizationConfig(dir)
623+
return err == nil
624+
}

0 commit comments

Comments
 (0)