diff --git a/Makefile b/Makefile index 3cb88527..df4a126c 100644 --- a/Makefile +++ b/Makefile @@ -60,7 +60,10 @@ update-deps-go: ## Update all golang dependencies for this repo install-helm: ## Install Helm toolchain for 3rd party integration ./hack/install-helm.sh -test: install-helm ## Runs golang unit tests +install-flux: + ./hack/install-flux.sh + +test: install-helm install-flux ## Runs golang unit tests ./hack/test-go.sh ##@ Helpers diff --git a/examples/third_party_integration/README.md b/examples/third_party_integration/README.md index e501d33a..ef921736 100644 --- a/examples/third_party_integration/README.md +++ b/examples/third_party_integration/README.md @@ -4,3 +4,4 @@ This section of the repository contains the example of how the third party tooli `e2e-framework` 1. [Helm](./helm) +2. [Flux](./flux) \ No newline at end of file diff --git a/examples/third_party_integration/flux/README.md b/examples/third_party_integration/flux/README.md new file mode 100644 index 00000000..c774c897 --- /dev/null +++ b/examples/third_party_integration/flux/README.md @@ -0,0 +1,31 @@ +# Flux Integration + +This section of the document gives you an example of how to integrate the flux workflow +into the `e2e-framework` while writing your tests. + +## Pre-Requisites + +1. `Flux` Installed on your system where the tests are being run for details visit flux official [website](https://fluxcd.io/). + +## Flux supported commands + +For the time being the framework supports following functionality: +- Flux installation and uninstallation. +- Handling [Kustomization](https://fluxcd.io/flux/components/kustomize/kustomization/) objects. +- Handling [GitRepository](https://fluxcd.io/flux/components/source/gitrepositories/) objects. + +## How does the example work? + +1. It creates a kind cluster with `flux` prefix. +2. Creates a namespace with `flux` prefix. +3. Installs all flux resources. +4. Creates a reference to the git repository, where a simple hello world application deployment is specified. You can find it [here](https://github.com/matrus2/go-hello-world). +5. Starts reconciliation by a flux kustomization manifest to path `template` of the git repository. +6. Assesses if the deployment of simple hello world app is up and running. +7. After the test passes it removes all resources. + +## How to run tests + +```shell +go test -c -o flux.test . && ./flux.test --v 4 +``` \ No newline at end of file diff --git a/examples/third_party_integration/flux/flux_test.go b/examples/third_party_integration/flux/flux_test.go new file mode 100644 index 00000000..725e1ebb --- /dev/null +++ b/examples/third_party_integration/flux/flux_test.go @@ -0,0 +1,53 @@ +/* +Copyright 2023 The Kubernetes Authors. + +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 flux + +import ( + "context" + "testing" + "time" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/e2e-framework/klient/wait" + "sigs.k8s.io/e2e-framework/klient/wait/conditions" + "sigs.k8s.io/e2e-framework/pkg/envconf" + "sigs.k8s.io/e2e-framework/pkg/features" +) + +func TestFluxRepoWorkflow(t *testing.T) { + feature := features.New("Install resources by flux"). + Assess("check if deployment was successful", func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context { + deployment := &appsv1.Deployment{ + ObjectMeta: v1.ObjectMeta{ + Name: "hello-app", + Namespace: c.Namespace(), + }, + Spec: appsv1.DeploymentSpec{}, + } + + err := wait.For(conditions.New(c.Client().Resources()).DeploymentConditionMatch(deployment, appsv1.DeploymentAvailable, corev1.ConditionStatus(v1.ConditionTrue)), wait.WithTimeout(time.Minute*5)) + if err != nil { + t.Fatal("Error deployment not found", err) + } + + return ctx + }).Feature() + + testEnv.Test(t, feature) +} diff --git a/examples/third_party_integration/flux/main_test.go b/examples/third_party_integration/flux/main_test.go new file mode 100644 index 00000000..eb6c54d1 --- /dev/null +++ b/examples/third_party_integration/flux/main_test.go @@ -0,0 +1,58 @@ +/* +Copyright 2023 The Kubernetes Authors. + +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 flux + +import ( + "os" + "testing" + + "sigs.k8s.io/e2e-framework/pkg/env" + "sigs.k8s.io/e2e-framework/pkg/envconf" + "sigs.k8s.io/e2e-framework/pkg/envfuncs" + "sigs.k8s.io/e2e-framework/third_party/flux" +) + +var ( + testEnv env.Environment + namespace string + kindClusterName string +) + +func TestMain(m *testing.M) { + cfg, _ := envconf.NewFromFlags() + testEnv = env.NewWithConfig(cfg) + kindClusterName = envconf.RandomName("flux", 10) + namespace = envconf.RandomName("flux", 10) + gitRepoName := "e2e-framework" + ksName := "hello-world" + testEnv.Setup( + envfuncs.CreateKindCluster(kindClusterName), + envfuncs.CreateNamespace(namespace), + flux.InstallFlux(), + flux.CreateGitRepo(gitRepoName, "https://github.com/kubernetes-sigs/e2e-framework", flux.WithBranch("main")), + flux.CreateKustomization(ksName, "GitRepository/"+gitRepoName+".flux-system", flux.WithPath("examples/third_party_integration/flux/template"), flux.WithArgs("--target-namespace", namespace, "--prune")), + ) + + testEnv.Finish( + flux.DeleteKustomization(ksName), + flux.DeleteGitRepo(gitRepoName), + flux.UninstallFlux(), + envfuncs.DeleteNamespace(namespace), + envfuncs.DestroyKindCluster(kindClusterName), + ) + os.Exit(testEnv.Run(m)) +} diff --git a/hack/install-flux.sh b/hack/install-flux.sh new file mode 100755 index 00000000..f8489ce4 --- /dev/null +++ b/hack/install-flux.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# Copyright 2023 The Kubernetes Authors. +# +# 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. + +set -e + +FLUX_URL=https://fluxcd.io/install.sh + +if ! command -v flux; then + # Running the piped command with sudo disabled to avoid any security concerns that might arise + curl -s $FLUX_URL | USE_SUDO=false bash +fi diff --git a/third_party/flux/cmd_manager.go b/third_party/flux/cmd_manager.go new file mode 100644 index 00000000..ca6da3d6 --- /dev/null +++ b/third_party/flux/cmd_manager.go @@ -0,0 +1,223 @@ +/* +Copyright 2023 The Kubernetes Authors. + +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 flux + +import ( + "fmt" + "strings" + + "github.com/vladimirvivien/gexe" + log "k8s.io/klog/v2" +) + +type Opts struct { + name string + source string + namespace string + mode string + url string + branch string + tag string + commit string + path string + interval string + args []string +} + +type Source string + +const ( + Git Source = "git" + Bucket Source = "bucket" + Helm Source = "helm" + Oci Source = "oci" +) + +type Manager struct { + e *gexe.Echo + kubeConfig string + path string +} + +type Option func(*Opts) + +func (m *Manager) processOpts(opts ...Option) *Opts { + option := &Opts{} + for _, op := range opts { + op(option) + } + return option +} + +// WithNamespace is used to specify the namespace of flux installation +func WithNamespace(namespace string) Option { + return func(opts *Opts) { + opts.namespace = namespace + } +} + +// WithCommit is used to target a source with a specific commit SHA +func WithCommit(commit string) Option { + return func(opts *Opts) { + opts.commit = commit + } +} + +// WithTag is used to target a source with a specific tag +func WithTag(tag string) Option { + return func(opts *Opts) { + opts.tag = tag + } +} + +// WithBranch is used to target a source with a specific branch +func WithBranch(branch string) Option { + return func(opts *Opts) { + opts.branch = branch + } +} + +// WithPath is used to specify a path for reconciliation +func WithPath(path string) Option { + return func(opts *Opts) { + opts.path = path + } +} + +// WithInterval is used to specify how often flux should check for changes in a source +func WithInterval(interval string) Option { + return func(opts *Opts) { + opts.interval = interval + } +} + +// WithArgs is used to pass any additional parameter to Flux command +func WithArgs(args ...string) Option { + return func(opts *Opts) { + opts.args = args + } +} + +func (m *Manager) run(opts *Opts) (err error) { + executable := "flux" + if m.path != "" { + executable = m.path + } + if m.e.Prog().Avail(executable) == "" { + err = fmt.Errorf("'flux' command is missing. Please ensure the tool exists before using the flux manager") + return + } + command := m.getCommand(opts) + + log.V(4).InfoS("Running Flux Operation", "command", command) + proc := m.e.RunProc(command) + result := proc.Result() + log.V(4).Info("Flux Command output \n", result) + if proc.IsSuccess() { + return nil + } else { + return proc.Err() + } +} + +func New(kubeConfig string) *Manager { + return &Manager{e: gexe.New(), kubeConfig: kubeConfig} +} + +func (m *Manager) getCommand(opt *Opts) string { + commandParts := []string{"flux", opt.mode} + + if opt.name != "" { + commandParts = append(commandParts, opt.name) + } + if opt.source != "" { + commandParts = append(commandParts, "--source", opt.source) + } + if opt.url != "" { + commandParts = append(commandParts, "--url", opt.url) + } + if opt.namespace != "" { + commandParts = append(commandParts, "--namespace", opt.namespace) + } + if opt.branch != "" { + commandParts = append(commandParts, "--branch", opt.branch) + } + if opt.tag != "" { + commandParts = append(commandParts, "--tag", opt.tag) + } + if opt.commit != "" { + commandParts = append(commandParts, "--commit", opt.commit) + } + if opt.path != "" { + commandParts = append(commandParts, "--path", opt.path) + } + if opt.interval != "" { + commandParts = append(commandParts, "--interval", opt.interval) + } + + commandParts = append(commandParts, opt.args...) + commandParts = append(commandParts, "--kubeconfig", m.kubeConfig) + return strings.Join(commandParts, " ") +} + +func (m *Manager) installFlux(opts ...Option) error { + o := m.processOpts(opts...) + o.mode = "install" + return m.run(o) +} + +func (m *Manager) uninstallFlux(opts ...Option) error { + o := m.processOpts(opts...) + o.mode = "uninstall -s" + return m.run(o) +} + +func (m *Manager) createSource(sourceType Source, name, url string, opts ...Option) error { + o := m.processOpts(opts...) + o.mode = string("create source " + sourceType) + o.name = name + o.url = url + return m.run(o) +} + +func (m *Manager) deleteSource(sourceType Source, name string, opts ...Option) error { + o := m.processOpts(opts...) + o.mode = string("delete source " + sourceType + " -s") + o.name = name + return m.run(o) +} + +func (m *Manager) createKustomization(name, source string, opts ...Option) error { + o := m.processOpts(opts...) + o.mode = "create ks" + o.name = name + o.source = source + return m.run(o) +} + +func (m *Manager) deleteKustomization(name string, opts ...Option) error { + o := m.processOpts(opts...) + o.mode = "delete ks -s" + o.name = name + return m.run(o) +} + +// WithPath is used to provide a custom path where the `flux` executable command can be found +func (m *Manager) WithPath(path string) *Manager { + m.path = path + return m +} diff --git a/third_party/flux/flux_setup.go b/third_party/flux/flux_setup.go new file mode 100644 index 00000000..20c48963 --- /dev/null +++ b/third_party/flux/flux_setup.go @@ -0,0 +1,111 @@ +/* +Copyright 2023 The Kubernetes Authors. + +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 flux + +import ( + "context" + "fmt" + + "sigs.k8s.io/e2e-framework/pkg/env" + "sigs.k8s.io/e2e-framework/pkg/envconf" +) + +var manager *Manager + +const NoFluxInstallationFoundMsg = "flux needs to be installed within a cluster first" + +// InstallFlux installs all flux components into the cluster. It is possible to specify a target namespace with flux.WithNamespace(). Default namespace is 'flux-system' +func InstallFlux(opts ...Option) env.Func { + return func(ctx context.Context, c *envconf.Config) (context.Context, error) { + manager = New(c.KubeconfigFile()) + err := manager.installFlux(opts...) + if err != nil { + return ctx, fmt.Errorf("installation of flux failed: %w", err) + } + return ctx, nil + } +} + +// CreateGitRepo creates a reference to a specific repository, it is a source for Kustomization or HelmRelease +func CreateGitRepo(gitRepoName, gitRepoURL string, opts ...Option) env.Func { + return func(ctx context.Context, c *envconf.Config) (context.Context, error) { + if manager == nil { + return ctx, fmt.Errorf(NoFluxInstallationFoundMsg) + } + err := manager.createSource(Git, gitRepoName, gitRepoURL, opts...) + if err != nil { + return ctx, fmt.Errorf("git reporistory creation failed: %w", err) + } + return ctx, nil + } +} + +// CreateKustomization is used to point to a specific source and path for reconciliation +func CreateKustomization(kustomizationName, sourceRef string, opts ...Option) env.Func { + return func(ctx context.Context, c *envconf.Config) (context.Context, error) { + if manager == nil { + return ctx, fmt.Errorf(NoFluxInstallationFoundMsg) + } + err := manager.createKustomization(kustomizationName, sourceRef, opts...) + if err != nil { + return ctx, fmt.Errorf("kustomization creation failed: %w", err) + } + return ctx, nil + } +} + +// UninstallFlux removes all flux components from a cluster +func UninstallFlux(opts ...Option) env.Func { + return func(ctx context.Context, c *envconf.Config) (context.Context, error) { + if manager == nil { + return ctx, fmt.Errorf(NoFluxInstallationFoundMsg) + } + err := manager.uninstallFlux(opts...) + if err != nil { + return ctx, fmt.Errorf("uninstallation of flux failed: %w", err) + } + return ctx, nil + } +} + +// DeleteKustomization removes a specific Kustomization object from the cluster +func DeleteKustomization(kustomizationName string, opts ...Option) env.Func { + return func(ctx context.Context, c *envconf.Config) (context.Context, error) { + if manager == nil { + return ctx, fmt.Errorf(NoFluxInstallationFoundMsg) + } + err := manager.deleteKustomization(kustomizationName, opts...) + if err != nil { + return ctx, fmt.Errorf("kustomization creation failed: %w", err) + } + return ctx, nil + } +} + +// DeleteGitRepo removes a specific GitRepository object from the cluster +func DeleteGitRepo(gitRepoName string, opts ...Option) env.Func { + return func(ctx context.Context, c *envconf.Config) (context.Context, error) { + if manager == nil { + return ctx, fmt.Errorf(NoFluxInstallationFoundMsg) + } + err := manager.deleteSource(Git, gitRepoName, opts...) + if err != nil { + return ctx, fmt.Errorf("git reporistory deletion failed: %w", err) + } + return ctx, nil + } +}