mirror of https://github.com/fluxcd/flux2.git
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
457 lines
12 KiB
Go
457 lines
12 KiB
Go
/*
|
|
Copyright 2021 The Flux 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 test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/google/go-containerregistry/pkg/crane"
|
|
git2go "github.com/libgit2/git2go/v33"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
apimeta "k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/client-go/kubernetes/scheme"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
|
|
|
helmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1"
|
|
automationv1beta1 "github.com/fluxcd/image-automation-controller/api/v1beta1"
|
|
reflectorv1beta1 "github.com/fluxcd/image-reflector-controller/api/v1beta1"
|
|
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2"
|
|
notiv1beta1 "github.com/fluxcd/notification-controller/api/v1beta1"
|
|
"github.com/fluxcd/pkg/apis/meta"
|
|
"github.com/fluxcd/pkg/git"
|
|
"github.com/fluxcd/pkg/git/libgit2"
|
|
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
|
|
"github.com/fluxcd/test-infra/tftestenv"
|
|
)
|
|
|
|
const defaultBranch = "main"
|
|
|
|
// fluxConfig contains configuration for installing FLux in a cluster
|
|
type fluxConfig struct {
|
|
kubeconfigPath string
|
|
repoURL string
|
|
password string
|
|
objects map[client.Object]controllerutil.MutateFn
|
|
kustomizeYaml string
|
|
}
|
|
|
|
func setupScheme() error {
|
|
err := sourcev1.AddToScheme(scheme.Scheme)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = kustomizev1.AddToScheme(scheme.Scheme)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = helmv2beta1.AddToScheme(scheme.Scheme)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = reflectorv1beta1.AddToScheme(scheme.Scheme)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = automationv1beta1.AddToScheme(scheme.Scheme)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = notiv1beta1.AddToScheme(scheme.Scheme)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// installFlux adds the core Flux components to the cluster specified in the kubeconfig file.
|
|
func installFlux(ctx context.Context, kubeClient client.Client, conf fluxConfig) error {
|
|
// Create flux-system namespace
|
|
namespace := corev1.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "flux-system",
|
|
},
|
|
}
|
|
err := testEnv.Client.Create(ctx, &namespace)
|
|
// create secret containing credentials for bootstrap repository
|
|
httpsCredentials := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "https-credentials", Namespace: "flux-system"}}
|
|
_, err = controllerutil.CreateOrUpdate(ctx, kubeClient, httpsCredentials, func() error {
|
|
httpsCredentials.StringData = map[string]string{
|
|
"username": "git",
|
|
"password": conf.password,
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create additional objects that are needed for flux to run correctly
|
|
for obj, fn := range conf.objects {
|
|
_, err = controllerutil.CreateOrUpdate(ctx, kubeClient, obj, fn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Install Flux and push files to git repository
|
|
gitClient, err := cloneRepository(conf.repoURL, "", conf.password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
repoDir := gitClient.Path()
|
|
err = tftestenv.RunCommand(ctx, repoDir, "mkdir -p ./clusters/e2e/flux-system", tftestenv.RunCommandOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = tftestenv.RunCommand(ctx, repoDir,
|
|
"flux install --components-extra=\"image-reflector-controller,image-automation-controller\" --export > ./clusters/e2e/flux-system/gotk-components.yaml",
|
|
tftestenv.RunCommandOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = tftestenv.RunCommand(ctx, repoDir, fmt.Sprintf("flux create source git flux-system --git-implementation=libgit2 --url=%s --branch=%s --secret-ref=https-credentials --interval=1m --export > ./clusters/e2e/flux-system/gotk-sync.yaml", conf.repoURL, defaultBranch),
|
|
tftestenv.RunCommandOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = tftestenv.RunCommand(ctx, repoDir, "flux create kustomization flux-system --source=flux-system --path='./clusters/e2e' --prune=true --interval=1m --export >> ./clusters/e2e/flux-system/gotk-sync.yaml", tftestenv.RunCommandOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
kustomizeYaml := `
|
|
resources:
|
|
- gotk-components.yaml
|
|
- gotk-sync.yaml
|
|
`
|
|
if conf.kustomizeYaml != "" {
|
|
kustomizeYaml = conf.kustomizeYaml
|
|
}
|
|
|
|
err = tftestenv.RunCommand(ctx, gitClient.Path(), fmt.Sprintf("echo \"%s\" > ./clusters/e2e/flux-system/kustomization.yaml", kustomizeYaml), tftestenv.RunCommandOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// commit and push manifests
|
|
err = commitAndPushAll(ctx, gitClient, defaultBranch, "Add sync manifests")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Need to apply CRDs first to make sure that the sync resources will apply properly
|
|
err = tftestenv.RunCommand(ctx, repoDir, fmt.Sprintf("kubectl --kubeconfig=%s apply -f ./clusters/e2e/flux-system/gotk-components.yaml", conf.kubeconfigPath), tftestenv.RunCommandOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = tftestenv.RunCommand(ctx, repoDir, fmt.Sprintf("kubectl --kubeconfig=%s apply -k ./clusters/e2e/flux-system/", conf.kubeconfigPath), tftestenv.RunCommandOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// verifyGitAndKustomization checks that the gitrespository and kustomization combination are working properly.
|
|
func verifyGitAndKustomization(ctx context.Context, kubeClient client.Client, namespace, name string) error {
|
|
nn := types.NamespacedName{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
}
|
|
source := &sourcev1.GitRepository{}
|
|
err := kubeClient.Get(ctx, nn, source)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if apimeta.IsStatusConditionPresentAndEqual(source.Status.Conditions, meta.ReadyCondition, metav1.ConditionTrue) == false {
|
|
return fmt.Errorf("source condition not ready")
|
|
}
|
|
kustomization := &kustomizev1.Kustomization{}
|
|
err = kubeClient.Get(ctx, nn, kustomization)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if apimeta.IsStatusConditionPresentAndEqual(kustomization.Status.Conditions, meta.ReadyCondition, metav1.ConditionTrue) == false {
|
|
return fmt.Errorf("kustomization condition not ready")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type nsConfig struct {
|
|
repoURL string
|
|
protocol string
|
|
objectName string
|
|
path string
|
|
modifyGitSpec func(spec *sourcev1.GitRepositorySpec)
|
|
modifyKsSpec func(spec *kustomizev1.KustomizationSpec)
|
|
}
|
|
|
|
// setupNamespaces creates the namespace, then creates the git secret,
|
|
// git repository and kustomization in that namespace
|
|
func setupNamespace(ctx context.Context, name string, opts nsConfig) error {
|
|
namespace := corev1.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
},
|
|
}
|
|
_, err := controllerutil.CreateOrUpdate(ctx, testEnv.Client, &namespace, func() error {
|
|
return nil
|
|
})
|
|
|
|
secret := corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "git-credentials",
|
|
Namespace: name,
|
|
},
|
|
}
|
|
|
|
secretData := map[string]string{
|
|
"username": "git",
|
|
"password": cfg.pat,
|
|
}
|
|
if opts.protocol == "ssh" {
|
|
secretData = map[string]string{
|
|
"identity": cfg.idRsa,
|
|
"identity.pub": cfg.idRsaPub,
|
|
"known_hosts": cfg.knownHosts,
|
|
}
|
|
}
|
|
|
|
_, err = controllerutil.CreateOrUpdate(ctx, testEnv.Client, &secret, func() error {
|
|
secret.StringData = secretData
|
|
return nil
|
|
})
|
|
|
|
gitSpec := &sourcev1.GitRepositorySpec{
|
|
Interval: metav1.Duration{
|
|
Duration: 1 * time.Minute,
|
|
},
|
|
Reference: &sourcev1.GitRepositoryRef{
|
|
Branch: name,
|
|
},
|
|
SecretRef: &meta.LocalObjectReference{
|
|
Name: secret.Name,
|
|
},
|
|
URL: opts.repoURL,
|
|
}
|
|
if infraOpts.Provider == "azure" {
|
|
gitSpec.GitImplementation = sourcev1.LibGit2Implementation
|
|
}
|
|
if opts.modifyGitSpec != nil {
|
|
opts.modifyGitSpec(gitSpec)
|
|
}
|
|
source := &sourcev1.GitRepository{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace.Name}}
|
|
_, err = controllerutil.CreateOrUpdate(ctx, testEnv.Client, source, func() error {
|
|
source.Spec = *gitSpec
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ksSpec := &kustomizev1.KustomizationSpec{
|
|
Path: opts.path,
|
|
SourceRef: kustomizev1.CrossNamespaceSourceReference{
|
|
Kind: sourcev1.GitRepositoryKind,
|
|
Name: source.Name,
|
|
Namespace: source.Namespace,
|
|
},
|
|
Interval: metav1.Duration{
|
|
Duration: 1 * time.Minute,
|
|
},
|
|
Prune: true,
|
|
}
|
|
if opts.modifyKsSpec != nil {
|
|
opts.modifyKsSpec(ksSpec)
|
|
}
|
|
kustomization := &kustomizev1.Kustomization{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace.Name}}
|
|
_, err = controllerutil.CreateOrUpdate(ctx, testEnv.Client, kustomization, func() error {
|
|
kustomization.Spec = *ksSpec
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cloneRepository(url, branchName string, password string) (*libgit2.Client, error) {
|
|
checkoutBranch := defaultBranch
|
|
|
|
tmpDir, err := os.MkdirTemp("", "*-repository")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
client, err := libgit2.NewClient(tmpDir, &git.AuthOptions{
|
|
Transport: git.HTTP,
|
|
Username: "git",
|
|
Password: password,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = client.Clone(context.Background(), url, git.CheckoutOptions{
|
|
Branch: checkoutBranch,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if branchName != "" && branchName != defaultBranch {
|
|
if err := client.SwitchBranch(context.Background(), branchName); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
func addFile(dir, path, content string) error {
|
|
err := os.WriteFile(filepath.Join(dir, path), []byte(content), 0777)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func commitAndPushAll(ctx context.Context, client *libgit2.Client, branch, message string) error {
|
|
_, err := client.Commit(git.Commit{
|
|
Author: git.Signature{
|
|
Name: "git",
|
|
Email: "test@example.com",
|
|
},
|
|
Message: message,
|
|
}, nil)
|
|
// no staged files error occurs when we are reusing infrastructure
|
|
// since the remote repository exists and is up to-date
|
|
if err != nil && !strings.Contains(err.Error(), "no staged files") {
|
|
return err
|
|
}
|
|
|
|
err = client.Push(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func createTagAndPush(client *libgit2.Client, branchName, tag, password string) error {
|
|
repo, err := git2go.OpenRepository(client.Path())
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
branch, err := repo.LookupBranch(branchName, git2go.BranchAll)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
commit, err := repo.LookupCommit(branch.Target())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tags, err := repo.Tags.List()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, existingTag := range tags {
|
|
if existingTag == tag {
|
|
err = repo.Tags.Remove(tag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
sig := &git2go.Signature{
|
|
Name: "git",
|
|
Email: "test@example.com",
|
|
When: time.Now(),
|
|
}
|
|
_, err = repo.Tags.Create(tag, commit, sig, "create tag")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
origin, err := repo.Remotes.Lookup("origin")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = origin.Push([]string{fmt.Sprintf("+refs/tags/%s", tag)}, &git2go.PushOptions{
|
|
RemoteCallbacks: git2go.RemoteCallbacks{
|
|
CredentialsCallback: credentialCallback("git", password),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func credentialCallback(username, password string) git2go.CredentialsCallback {
|
|
return func(url string, usernameFromURL string, allowedTypes git2go.CredType) (*git2go.Cred, error) {
|
|
cred, err := git2go.NewCredentialUserpassPlaintext(username, password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return cred, nil
|
|
}
|
|
}
|
|
|
|
func executeTemplate(path string, templateValues map[string]string) (string, error) {
|
|
buf, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
tmpl := template.Must(template.New("golden").Parse(string(buf)))
|
|
var out bytes.Buffer
|
|
if err := tmpl.Execute(&out, templateValues); err != nil {
|
|
return "", err
|
|
}
|
|
return out.String(), nil
|
|
}
|
|
|
|
func pushImagesFromURL(repoURL, imgURL string, tags []string) error {
|
|
img, err := crane.Pull(imgURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, tag := range tags {
|
|
log.Printf("Pushing '%s' to '%s:%s'\n", imgURL, repoURL, tag)
|
|
if err := crane.Push(img, fmt.Sprintf("%s:%s", repoURL, tag)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|