diff --git a/pkg/install/install.go b/pkg/install/install.go new file mode 100644 index 00000000..e7b499aa --- /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) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), options.Timeout) + defer cancel() + + tmpDir, err := ioutil.TempDir("", options.Namespace) + if err != nil { + return "", 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 "", err + } + } else { + if err := fetch(ctx, options.BaseURL, options.Version, tmpDir); err != nil { + return "", err + } + + if err := generate(tmpDir, options); err != nil { + return "", err + } + + if err := build(tmpDir, output); err != nil { + return "", err + } + } + + content, err := ioutil.ReadFile(output) + if err != nil { + return "", err + } + + return string(content), nil +} diff --git a/pkg/install/install_test.go b/pkg/install/install_test.go new file mode 100644 index 00000000..765d8c8f --- /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(output, img) { + t.Errorf("component image '%s' not found", img) + } + } + + fmt.Println(output) +} diff --git a/pkg/install/manifests.go b/pkg/install/manifests.go new file mode 100644 index 00000000..e9dcfac3 --- /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 00000000..a15410d3 --- /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 00000000..4cc1b357 --- /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() +}