diff --git a/cmd/gotk/bootstrap.go b/cmd/gotk/bootstrap.go
index b82a704ee032f9b08d3d127d3ac96619d43e0c3b..91c3fa22c15d289dae780fe2523b7c4e92b0ebd9 100644
--- a/cmd/gotk/bootstrap.go
+++ b/cmd/gotk/bootstrap.go
@@ -19,6 +19,7 @@ package main
 import (
 	"context"
 	"fmt"
+	"io/ioutil"
 	"net/url"
 	"os"
 	"path"
@@ -36,6 +37,8 @@ import (
 
 	kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1"
 	sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
+
+	"github.com/fluxcd/toolkit/pkg/install"
 )
 
 var bootstrapCmd = &cobra.Command{
@@ -111,31 +114,36 @@ func generateInstallManifests(targetPath, namespace, tmpDir string, localManifes
 	if err := os.MkdirAll(manifestsDir, os.ModePerm); err != nil {
 		return "", fmt.Errorf("creating manifests dir failed: %w", err)
 	}
-	manifest := path.Join(manifestsDir, bootstrapInstallManifest)
 
-	if localManifests != "" {
-		if err := buildKustomization(localManifests, manifest); err != nil {
-			return "", fmt.Errorf("build kustomization failed: %w", err)
-		}
+	manifest := path.Join(manifestsDir, bootstrapInstallManifest)
 
-		return manifest, nil
+	opts := install.Options{
+		BaseURL:                localManifests,
+		Version:                bootstrapVersion,
+		Namespace:              namespace,
+		Components:             bootstrapComponents,
+		Registry:               bootstrapRegistry,
+		ImagePullSecret:        bootstrapImagePullSecret,
+		Arch:                   bootstrapArch,
+		WatchAllNamespaces:     bootstrapWatchAllNamespaces,
+		NetworkPolicy:          bootstrapNetworkPolicy,
+		LogLevel:               bootstrapLogLevel,
+		NotificationController: defaultNotification,
+		ManifestsFile:          fmt.Sprintf("%s.yaml", namespace),
+		Timeout:                timeout,
 	}
 
-	gotkDir := path.Join(tmpDir, ".gotk")
-	defer os.RemoveAll(gotkDir)
-
-	if err := os.MkdirAll(gotkDir, os.ModePerm); err != nil {
-		return "", fmt.Errorf("generating manifests failed: %w", err)
+	if localManifests == "" {
+		opts.BaseURL = install.MakeDefaultOptions().BaseURL
 	}
 
-	if err := genInstallManifests(bootstrapVersion, namespace, bootstrapComponents,
-		bootstrapWatchAllNamespaces, bootstrapNetworkPolicy, bootstrapRegistry, bootstrapImagePullSecret,
-		bootstrapArch, bootstrapLogLevel, gotkDir); err != nil {
-		return "", fmt.Errorf("generating manifests failed: %w", err)
+	output, err := install.Generate(opts)
+	if err != nil {
+		return "", fmt.Errorf("generating install manifests failed: %w", err)
 	}
 
-	if err := buildKustomization(gotkDir, manifest); err != nil {
-		return "", fmt.Errorf("build kustomization failed: %w", err)
+	if err := ioutil.WriteFile(manifest, output, os.ModePerm); err != nil {
+		return "", fmt.Errorf("generating install manifests failed: %w", err)
 	}
 
 	return manifest, nil
diff --git a/cmd/gotk/install.go b/cmd/gotk/install.go
index 5ecc3b596a3d61e6dc6050b3b5418b10fcbaf99b..5aefbcb27efc2d2334085992b60f64832b47d622 100644
--- a/cmd/gotk/install.go
+++ b/cmd/gotk/install.go
@@ -20,18 +20,13 @@ import (
 	"context"
 	"fmt"
 	"io/ioutil"
-	"net/http"
 	"os"
 	"path"
-	"path/filepath"
 	"strings"
-	"time"
 
 	"github.com/spf13/cobra"
-	"sigs.k8s.io/kustomize/api/filesys"
-	"sigs.k8s.io/kustomize/api/krusty"
 
-	"github.com/fluxcd/pkg/untar"
+	"github.com/fluxcd/toolkit/pkg/install"
 )
 
 var installCmd = &cobra.Command{
@@ -114,38 +109,50 @@ func installCmdRun(cmd *cobra.Command, args []string) error {
 	if !installExport {
 		logger.Generatef("generating manifests")
 	}
+
+	opts := install.Options{
+		BaseURL:                installManifestsPath,
+		Version:                installVersion,
+		Namespace:              namespace,
+		Components:             installComponents,
+		Registry:               installRegistry,
+		ImagePullSecret:        installImagePullSecret,
+		Arch:                   installArch,
+		WatchAllNamespaces:     installWatchAllNamespaces,
+		NetworkPolicy:          installNetworkPolicy,
+		LogLevel:               installLogLevel,
+		NotificationController: defaultNotification,
+		ManifestsFile:          fmt.Sprintf("%s.yaml", namespace),
+		Timeout:                timeout,
+	}
+
 	if installManifestsPath == "" {
-		err = genInstallManifests(installVersion, namespace, installComponents,
-			installWatchAllNamespaces, installNetworkPolicy, installRegistry, installImagePullSecret,
-			installArch, installLogLevel, tmpDir)
-		if err != nil {
-			return fmt.Errorf("install failed: %w", err)
-		}
-		installManifestsPath = tmpDir
+		opts.BaseURL = install.MakeDefaultOptions().BaseURL
 	}
 
-	manifest := path.Join(tmpDir, fmt.Sprintf("%s.yaml", namespace))
-	if err := buildKustomization(installManifestsPath, manifest); err != nil {
+	output, err := install.Generate(opts)
+	if err != nil {
 		return fmt.Errorf("install failed: %w", err)
 	}
 
-	command := fmt.Sprintf("cat %s", manifest)
-	if yaml, err := utils.execCommand(ctx, ModeCapture, command); err != nil {
+	manifest := path.Join(tmpDir, fmt.Sprintf("%s.yaml", namespace))
+	if err := ioutil.WriteFile(manifest, output, os.ModePerm); err != nil {
 		return fmt.Errorf("install failed: %w", err)
-	} else {
-		if verbose {
-			fmt.Print(yaml)
-		} else if installExport {
-			fmt.Println("---")
-			fmt.Println("# GitOps Toolkit revision", installVersion, time.Now().Format(time.RFC3339))
-			fmt.Println("# Components:", strings.Join(installComponents, ","))
-			fmt.Print(yaml)
-			fmt.Println("---")
-			return nil
-		}
 	}
-	logger.Successf("manifests build completed")
 
+	yaml := string(output)
+	if verbose {
+		fmt.Print(yaml)
+	} else if installExport {
+		fmt.Println("---")
+		fmt.Println("# GitOps Toolkit revision", installVersion)
+		fmt.Println("# Components:", strings.Join(installComponents, ","))
+		fmt.Print(yaml)
+		fmt.Println("---")
+		return nil
+	}
+
+	logger.Successf("manifests build completed")
 	logger.Actionf("installing components in %s namespace", namespace)
 	applyOutput := ModeStderrOS
 	if verbose {
@@ -156,7 +163,8 @@ func installCmdRun(cmd *cobra.Command, args []string) error {
 		dryRun = "--dry-run=client"
 		applyOutput = ModeOS
 	}
-	command = fmt.Sprintf("cat %s | kubectl apply -f- %s", manifest, dryRun)
+
+	command := fmt.Sprintf("kubectl apply -f %s %s", manifest, dryRun)
 	if _, err := utils.execCommand(ctx, applyOutput, command); err != nil {
 		return fmt.Errorf("install failed")
 	}
@@ -182,250 +190,3 @@ func installCmdRun(cmd *cobra.Command, args []string) error {
 	logger.Successf("install finished")
 	return nil
 }
-
-var namespaceTmpl = `---
-apiVersion: v1
-kind: Namespace
-metadata:
-  name: {{.Namespace}}
-`
-
-var labelsTmpl = `---
-apiVersion: builtin
-kind: LabelTransformer
-metadata:
-  name: labels
-labels:
-  app.kubernetes.io/instance: {{.Namespace}}
-  app.kubernetes.io/version: "{{.Version}}"
-fieldSpecs:
-  - path: metadata/labels
-    create: true
-`
-
-var kustomizationTmpl = `---
-{{- $eventsAddr := .EventsAddr }}
-{{- $watchAllNamespaces := .WatchAllNamespaces }}
-{{- $registry := .Registry }}
-{{- $arch := .Arch }}
-{{- $logLevel := .LogLevel }}
-apiVersion: kustomize.config.k8s.io/v1beta1
-kind: Kustomization
-namespace: {{.Namespace}}
-
-transformers:
-  - labels.yaml
-
-resources:
-  - namespace.yaml
-{{- if .NetworkPolicy }}
-  - policies.yaml
-{{- end }}
-  - roles
-{{- range .Components }}
-  - {{.}}.yaml
-{{- end }}
-
-patches:
-- path: node-selector.yaml
-  target:
-    kind: Deployment
-
-patchesJson6902:
-{{- range $i, $component := .Components }}
-{{- if eq $component "notification-controller" }}
-- target:
-    group: apps
-    version: v1
-    kind: Deployment
-    name: {{$component}}
-  patch: |-
-    - op: replace
-      path: /spec/template/spec/containers/0/args/0
-      value: --watch-all-namespaces={{$watchAllNamespaces}}
-    - op: replace
-      path: /spec/template/spec/containers/0/args/1
-      value: --log-level={{$logLevel}}
-{{- else }}
-- target:
-    group: apps
-    version: v1
-    kind: Deployment
-    name: {{$component}}
-  patch: |-
-    - op: replace
-      path: /spec/template/spec/containers/0/args/0
-      value: --events-addr={{$eventsAddr}}
-    - op: replace
-      path: /spec/template/spec/containers/0/args/1
-      value: --watch-all-namespaces={{$watchAllNamespaces}}
-    - op: replace
-      path: /spec/template/spec/containers/0/args/2
-      value: --log-level={{$logLevel}}
-{{- end }}
-{{- end }}
-
-{{- if $registry }}
-images:
-{{- range $i, $component := .Components }}
-  - name: fluxcd/{{$component}}
-{{- if eq $arch "amd64" }}
-    newName: {{$registry}}/{{$component}}
-{{- else }}
-    newName: {{$registry}}/{{$component}}-arm64
-{{- end }}
-{{- end }}
-{{- end }}
-`
-
-var kustomizationRolesTmpl = `---
-apiVersion: kustomize.config.k8s.io/v1beta1
-kind: Kustomization
-resources:
-  - rbac.yaml
-nameSuffix: -{{.Namespace}}
-`
-
-var nodeSelectorTmpl = `---
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: all
-spec:
-  template:
-    spec:
-      nodeSelector:
-        kubernetes.io/arch: {{.Arch}}
-        kubernetes.io/os: linux
-{{- if .ImagePullSecret }}
-      imagePullSecrets:
-       - name: {{.ImagePullSecret}}
-{{- end }}
-`
-
-func downloadManifests(version string, tmpDir string) error {
-	ghURL := "https://github.com/fluxcd/toolkit/releases/latest/download/manifests.tar.gz"
-	if strings.HasPrefix(version, "v") {
-		ghURL = fmt.Sprintf("https://github.com/fluxcd/toolkit/releases/download/%s/manifests.tar.gz", version)
-	}
-
-	ctx, cancel := context.WithTimeout(context.Background(), timeout)
-	defer cancel()
-
-	req, err := http.NewRequest("GET", ghURL, nil)
-	if err != nil {
-		return fmt.Errorf("failed to create HTTP request for %s, error: %w", ghURL, err)
-	}
-
-	// download
-	resp, err := http.DefaultClient.Do(req.WithContext(ctx))
-	if err != nil {
-		return fmt.Errorf("failed to download artifact from %s, error: %w", ghURL, err)
-	}
-	defer resp.Body.Close()
-
-	// check response
-	if resp.StatusCode != http.StatusOK {
-		return fmt.Errorf("faild to download artifact from %s, status: %s", ghURL, resp.Status)
-	}
-
-	// extract
-	if _, err = untar.Untar(resp.Body, tmpDir); err != nil {
-		return fmt.Errorf("faild to untar manifests from %s, error: %w", ghURL, err)
-	}
-
-	return nil
-}
-
-func genInstallManifests(version string, namespace string, components []string,
-	watchAllNamespaces, networkPolicy bool, registry, imagePullSecret, arch, logLevel, tmpDir string) error {
-	eventsAddr := ""
-	if utils.containsItemString(components, defaultNotification) {
-		eventsAddr = fmt.Sprintf("http://%s/", defaultNotification)
-	}
-
-	model := struct {
-		Version            string
-		Namespace          string
-		Components         []string
-		EventsAddr         string
-		Registry           string
-		ImagePullSecret    string
-		Arch               string
-		WatchAllNamespaces bool
-		NetworkPolicy      bool
-		LogLevel           string
-	}{
-		Version:            version,
-		Namespace:          namespace,
-		Components:         components,
-		EventsAddr:         eventsAddr,
-		Registry:           registry,
-		ImagePullSecret:    imagePullSecret,
-		Arch:               arch,
-		WatchAllNamespaces: watchAllNamespaces,
-		NetworkPolicy:      networkPolicy,
-		LogLevel:           logLevel,
-	}
-
-	if err := downloadManifests(version, tmpDir); err != nil {
-		return err
-	}
-
-	if err := utils.execTemplate(model, namespaceTmpl, path.Join(tmpDir, "namespace.yaml")); err != nil {
-		return fmt.Errorf("generate namespace failed: %w", err)
-	}
-
-	if err := utils.execTemplate(model, labelsTmpl, path.Join(tmpDir, "labels.yaml")); err != nil {
-		return fmt.Errorf("generate labels failed: %w", err)
-	}
-
-	if err := utils.execTemplate(model, nodeSelectorTmpl, path.Join(tmpDir, "node-selector.yaml")); err != nil {
-		return fmt.Errorf("generate node selector failed: %w", err)
-	}
-
-	if err := utils.execTemplate(model, kustomizationTmpl, path.Join(tmpDir, "kustomization.yaml")); err != nil {
-		return fmt.Errorf("generate kustomization failed: %w", err)
-	}
-
-	if err := os.MkdirAll(path.Join(tmpDir, "roles"), os.ModePerm); err != nil {
-		return fmt.Errorf("generate roles failed: %w", err)
-	}
-
-	if err := utils.execTemplate(model, kustomizationRolesTmpl, path.Join(tmpDir, "roles/kustomization.yaml")); err != nil {
-		return fmt.Errorf("generate roles failed: %w", err)
-	}
-
-	if err := utils.copyFile(filepath.Join(tmpDir, "rbac.yaml"), filepath.Join(tmpDir, "roles/rbac.yaml")); err != nil {
-		return fmt.Errorf("generate rbac failed: %w", err)
-	}
-
-	return nil
-}
-
-func buildKustomization(base, manifests string) error {
-	kfile := filepath.Join(base, "kustomization.yaml")
-
-	fs := filesys.MakeFsOnDisk()
-	if !fs.Exists(kfile) {
-		return fmt.Errorf("%s not found", kfile)
-	}
-
-	opt := krusty.MakeDefaultOptions()
-	k := krusty.MakeKustomizer(fs, opt)
-	m, err := k.Run(base)
-	if err != nil {
-		return err
-	}
-
-	resources, err := m.AsYaml()
-	if err != nil {
-		return err
-	}
-
-	if err := fs.WriteFile(manifests, resources); err != nil {
-		return err
-	}
-
-	return nil
-}
diff --git a/pkg/install/install.go b/pkg/install/install.go
new file mode 100644
index 0000000000000000000000000000000000000000..0d2673948aa0215b563db98e7bbeee512eecd384
--- /dev/null
+++ b/pkg/install/install.go
@@ -0,0 +1,67 @@
+/*
+Copyright 2020 The Flux CD contributors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package install
+
+import (
+	"context"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path"
+	"strings"
+)
+
+// Generate returns the install manifests as a multi-doc YAML.
+// The manifests are built from a GitHub release or from a
+// Kustomize overlay if the supplied Options.BaseURL is a local path.
+func Generate(options Options) ([]byte, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), options.Timeout)
+	defer cancel()
+
+	tmpDir, err := ioutil.TempDir("", options.Namespace)
+	if err != nil {
+		return nil, fmt.Errorf("temp dir error: %w", err)
+	}
+	defer os.RemoveAll(tmpDir)
+
+	output := path.Join(tmpDir, options.ManifestsFile)
+
+	if !strings.HasPrefix(options.BaseURL, "http") {
+		if err := build(options.BaseURL, output); err != nil {
+			return nil, err
+		}
+	} else {
+		if err := fetch(ctx, options.BaseURL, options.Version, tmpDir); err != nil {
+			return nil, err
+		}
+
+		if err := generate(tmpDir, options); err != nil {
+			return nil, err
+		}
+
+		if err := build(tmpDir, output); err != nil {
+			return nil, err
+		}
+	}
+
+	content, err := ioutil.ReadFile(output)
+	if err != nil {
+		return nil, err
+	}
+
+	return content, nil
+}
diff --git a/pkg/install/install_test.go b/pkg/install/install_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..b60d17997537d239fa01aee361201a66d7dbfedb
--- /dev/null
+++ b/pkg/install/install_test.go
@@ -0,0 +1,40 @@
+/*
+Copyright 2020 The Flux CD contributors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package install
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+)
+
+func TestGenerate(t *testing.T) {
+	opts := MakeDefaultOptions()
+	output, err := Generate(opts)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	for _, component := range opts.Components {
+		img := fmt.Sprintf("%s/%s", opts.Registry, component)
+		if !strings.Contains(string(output), img) {
+			t.Errorf("component image '%s' not found", img)
+		}
+	}
+
+	fmt.Println(string(output))
+}
diff --git a/pkg/install/manifests.go b/pkg/install/manifests.go
new file mode 100644
index 0000000000000000000000000000000000000000..e9dcfac3783c3ec08a615e82eb2ba5d0abbc8de7
--- /dev/null
+++ b/pkg/install/manifests.go
@@ -0,0 +1,125 @@
+/*
+Copyright 2020 The Flux CD contributors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package install
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+
+	"sigs.k8s.io/kustomize/api/filesys"
+	"sigs.k8s.io/kustomize/api/krusty"
+
+	"github.com/fluxcd/pkg/untar"
+)
+
+func fetch(ctx context.Context, url, version, dir string) error {
+	ghURL := fmt.Sprintf("%s/latest/download/manifests.tar.gz", url)
+	if strings.HasPrefix(version, "v") {
+		ghURL = fmt.Sprintf("%s/download/%s/manifests.tar.gz", url, version)
+	}
+
+	req, err := http.NewRequest("GET", ghURL, nil)
+	if err != nil {
+		return fmt.Errorf("failed to create HTTP request for %s, error: %w", ghURL, err)
+	}
+
+	// download
+	resp, err := http.DefaultClient.Do(req.WithContext(ctx))
+	if err != nil {
+		return fmt.Errorf("failed to download manifests.tar.gz from %s, error: %w", ghURL, err)
+	}
+	defer resp.Body.Close()
+
+	// check response
+	if resp.StatusCode != http.StatusOK {
+		return fmt.Errorf("faild to download manifests.tar.gz from %s, status: %s", ghURL, resp.Status)
+	}
+
+	// extract
+	if _, err = untar.Untar(resp.Body, dir); err != nil {
+		return fmt.Errorf("faild to untar manifests.tar.gz from %s, error: %w", ghURL, err)
+	}
+
+	return nil
+}
+
+func generate(base string, options Options) error {
+	if containsItemString(options.Components, options.NotificationController) {
+		options.EventsAddr = fmt.Sprintf("http://%s/", options.NotificationController)
+	}
+
+	if err := execTemplate(options, namespaceTmpl, path.Join(base, "namespace.yaml")); err != nil {
+		return fmt.Errorf("generate namespace failed: %w", err)
+	}
+
+	if err := execTemplate(options, labelsTmpl, path.Join(base, "labels.yaml")); err != nil {
+		return fmt.Errorf("generate labels failed: %w", err)
+	}
+
+	if err := execTemplate(options, nodeSelectorTmpl, path.Join(base, "node-selector.yaml")); err != nil {
+		return fmt.Errorf("generate node selector failed: %w", err)
+	}
+
+	if err := execTemplate(options, kustomizationTmpl, path.Join(base, "kustomization.yaml")); err != nil {
+		return fmt.Errorf("generate kustomization failed: %w", err)
+	}
+
+	if err := os.MkdirAll(path.Join(base, "roles"), os.ModePerm); err != nil {
+		return fmt.Errorf("generate roles failed: %w", err)
+	}
+
+	if err := execTemplate(options, kustomizationRolesTmpl, path.Join(base, "roles/kustomization.yaml")); err != nil {
+		return fmt.Errorf("generate roles kustomization failed: %w", err)
+	}
+
+	if err := copyFile(filepath.Join(base, "rbac.yaml"), filepath.Join(base, "roles/rbac.yaml")); err != nil {
+		return fmt.Errorf("generate rbac failed: %w", err)
+	}
+	return nil
+}
+
+func build(base, output string) error {
+	kfile := filepath.Join(base, "kustomization.yaml")
+
+	fs := filesys.MakeFsOnDisk()
+	if !fs.Exists(kfile) {
+		return fmt.Errorf("%s not found", kfile)
+	}
+
+	opt := krusty.MakeDefaultOptions()
+	k := krusty.MakeKustomizer(fs, opt)
+	m, err := k.Run(base)
+	if err != nil {
+		return err
+	}
+
+	resources, err := m.AsYaml()
+	if err != nil {
+		return err
+	}
+
+	if err := fs.WriteFile(output, resources); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/install/options.go b/pkg/install/options.go
new file mode 100644
index 0000000000000000000000000000000000000000..a15410d3aba8099b6c0d33c21647511622d8364c
--- /dev/null
+++ b/pkg/install/options.go
@@ -0,0 +1,64 @@
+/*
+Copyright 2020 The Flux CD contributors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package install
+
+import "time"
+
+type Options struct {
+	BaseURL                string
+	Version                string
+	Namespace              string
+	Components             []string
+	EventsAddr             string
+	Registry               string
+	ImagePullSecret        string
+	Arch                   string
+	WatchAllNamespaces     bool
+	NetworkPolicy          bool
+	LogLevel               string
+	NotificationController string
+	ManifestsFile          string
+	Timeout                time.Duration
+}
+
+func MakeDefaultOptions() Options {
+	return Options{
+		Version:                "latest",
+		Namespace:              "gotk-system",
+		Components:             []string{"source-controller", "kustomize-controller", "helm-controller", "notification-controller"},
+		EventsAddr:             "",
+		Registry:               "ghcr.io/fluxcd",
+		ImagePullSecret:        "",
+		Arch:                   "amd64",
+		WatchAllNamespaces:     true,
+		NetworkPolicy:          true,
+		LogLevel:               "info",
+		BaseURL:                "https://github.com/fluxcd/toolkit/releases",
+		NotificationController: "notification-controller",
+		ManifestsFile:          "toolkit-components.yaml",
+		Timeout:                time.Minute,
+	}
+}
+
+func containsItemString(s []string, e string) bool {
+	for _, a := range s {
+		if a == e {
+			return true
+		}
+	}
+	return false
+}
diff --git a/pkg/install/templates.go b/pkg/install/templates.go
new file mode 100644
index 0000000000000000000000000000000000000000..4cc1b35739a1181b79c2e0916b79d57ec3f9d1ea
--- /dev/null
+++ b/pkg/install/templates.go
@@ -0,0 +1,195 @@
+/*
+Copyright 2020 The Flux CD contributors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package install
+
+import (
+	"bufio"
+	"bytes"
+	"io"
+	"os"
+	"text/template"
+)
+
+var kustomizationTmpl = `---
+{{- $eventsAddr := .EventsAddr }}
+{{- $watchAllNamespaces := .WatchAllNamespaces }}
+{{- $registry := .Registry }}
+{{- $arch := .Arch }}
+{{- $logLevel := .LogLevel }}
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: {{.Namespace}}
+
+transformers:
+  - labels.yaml
+
+resources:
+  - namespace.yaml
+{{- if .NetworkPolicy }}
+  - policies.yaml
+{{- end }}
+  - roles
+{{- range .Components }}
+  - {{.}}.yaml
+{{- end }}
+
+patches:
+- path: node-selector.yaml
+  target:
+    kind: Deployment
+
+patchesJson6902:
+{{- range $i, $component := .Components }}
+{{- if eq $component "notification-controller" }}
+- target:
+    group: apps
+    version: v1
+    kind: Deployment
+    name: {{$component}}
+  patch: |-
+    - op: replace
+      path: /spec/template/spec/containers/0/args/0
+      value: --watch-all-namespaces={{$watchAllNamespaces}}
+    - op: replace
+      path: /spec/template/spec/containers/0/args/1
+      value: --log-level={{$logLevel}}
+{{- else }}
+- target:
+    group: apps
+    version: v1
+    kind: Deployment
+    name: {{$component}}
+  patch: |-
+    - op: replace
+      path: /spec/template/spec/containers/0/args/0
+      value: --events-addr={{$eventsAddr}}
+    - op: replace
+      path: /spec/template/spec/containers/0/args/1
+      value: --watch-all-namespaces={{$watchAllNamespaces}}
+    - op: replace
+      path: /spec/template/spec/containers/0/args/2
+      value: --log-level={{$logLevel}}
+{{- end }}
+{{- end }}
+
+{{- if $registry }}
+images:
+{{- range $i, $component := .Components }}
+  - name: fluxcd/{{$component}}
+{{- if eq $arch "amd64" }}
+    newName: {{$registry}}/{{$component}}
+{{- else }}
+    newName: {{$registry}}/{{$component}}-arm64
+{{- end }}
+{{- end }}
+{{- end }}
+`
+
+var kustomizationRolesTmpl = `---
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+  - rbac.yaml
+nameSuffix: -{{.Namespace}}
+`
+
+var nodeSelectorTmpl = `---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: all
+spec:
+  template:
+    spec:
+      nodeSelector:
+        kubernetes.io/arch: {{.Arch}}
+        kubernetes.io/os: linux
+{{- if .ImagePullSecret }}
+      imagePullSecrets:
+       - name: {{.ImagePullSecret}}
+{{- end }}
+`
+
+var labelsTmpl = `---
+apiVersion: builtin
+kind: LabelTransformer
+metadata:
+  name: labels
+labels:
+  app.kubernetes.io/instance: {{.Namespace}}
+  app.kubernetes.io/version: "{{.Version}}"
+fieldSpecs:
+  - path: metadata/labels
+    create: true
+`
+
+var namespaceTmpl = `---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: {{.Namespace}}
+`
+
+func execTemplate(obj interface{}, tmpl, filename string) error {
+	t, err := template.New("tmpl").Parse(tmpl)
+	if err != nil {
+		return err
+	}
+
+	var data bytes.Buffer
+	writer := bufio.NewWriter(&data)
+	if err := t.Execute(writer, obj); err != nil {
+		return err
+	}
+
+	if err := writer.Flush(); err != nil {
+		return err
+	}
+
+	file, err := os.Create(filename)
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+
+	_, err = io.WriteString(file, data.String())
+	if err != nil {
+		return err
+	}
+
+	return file.Sync()
+}
+
+func copyFile(src, dst string) error {
+	in, err := os.Open(src)
+	if err != nil {
+		return err
+	}
+	defer in.Close()
+
+	out, err := os.Create(dst)
+	if err != nil {
+		return err
+	}
+	defer out.Close()
+
+	_, err = io.Copy(out, in)
+	if err != nil {
+		return err
+	}
+	return out.Close()
+}