blob: 204865f41a60ccc1c931a739184d8e970877d030 [file] [log] [blame]
// Copyright Istio 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 mesh
import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"strings"
)
import (
"istio.io/pkg/log"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/yaml"
)
import (
"github.com/apache/dubbo-go-pixiu/operator/pkg/apis/istio/v1alpha1"
"github.com/apache/dubbo-go-pixiu/operator/pkg/cache"
"github.com/apache/dubbo-go-pixiu/operator/pkg/controller/istiocontrolplane"
"github.com/apache/dubbo-go-pixiu/operator/pkg/helmreconciler"
"github.com/apache/dubbo-go-pixiu/operator/pkg/manifest"
"github.com/apache/dubbo-go-pixiu/operator/pkg/name"
"github.com/apache/dubbo-go-pixiu/operator/pkg/object"
"github.com/apache/dubbo-go-pixiu/operator/pkg/util/clog"
"github.com/apache/dubbo-go-pixiu/pkg/kube"
)
// cmdType is one of the commands used to generate and optionally apply a manifest.
type cmdType string
const (
// istioctl manifest generate
cmdGenerate cmdType = "istioctl manifest generate"
// istioctl install
cmdApply cmdType = "istioctl install"
// in-cluster controller
cmdController cmdType = "operator controller"
)
// Golden output files add a lot of noise to pull requests. Use a unique suffix so
// we can hide them by default. This should match one of the `linuguist-generated=true`
// lines in istio.io/istio/.gitattributes.
const (
goldenFileSuffixHideChangesInReview = ".golden.yaml"
goldenFileSuffixShowChangesInReview = ".golden-show-in-gh-pull-request.yaml"
)
var (
// By default, tests only run with manifest generate, since it doesn't require any external fake test environment.
testedManifestCmds = []cmdType{cmdGenerate}
// Only used if kubebuilder is installed.
testenv *envtest.Environment
testClient client.Client
testReconcileOperator *istiocontrolplane.ReconcileIstioOperator
allNamespacedGVKs = append(helmreconciler.NamespacedResources(&version.Info{Major: "1", Minor: "25"}),
schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Endpoints"})
// CRDs are not in the prune list, but must be considered for tests.
allClusterGVKs = append(helmreconciler.ClusterResources,
schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1beta1", Kind: "CustomResourceDefinition"})
)
func init() {
if kubeBuilderInstalled() {
// TestMode is required to not wait in the go client for resources that will never be created in the test server.
helmreconciler.TestMode = true
// Add install and controller to the list of commands to run tests against.
testedManifestCmds = append(testedManifestCmds, cmdApply, cmdController)
}
}
// recreateTestEnv (re)creates a kubebuilder fake API server environment. This is required for testing of the
// controller runtime, which is used in the operator.
func recreateTestEnv() error {
// If kubebuilder is installed, use that test env for apply and controller testing.
log.Infof("Recreating kubebuilder test environment\n")
if testenv != nil {
testenv.Stop()
}
var err error
testenv = &envtest.Environment{}
testRestConfig, err = testenv.Start()
if err != nil {
return err
}
testK8Interface, err = kubernetes.NewForConfig(testRestConfig)
testRestConfig.QPS = 50
testRestConfig.Burst = 100
if err != nil {
return err
}
s := scheme.Scheme
s.AddKnownTypes(v1alpha1.SchemeGroupVersion, &v1alpha1.IstioOperator{})
testClient, err = client.New(testRestConfig, client.Options{Scheme: s})
if err != nil {
return err
}
testReconcileOperator = istiocontrolplane.NewReconcileIstioOperator(testClient, nil, s)
return nil
}
// recreateSimpleTestEnv mocks fake kube api server which relies on a simple object tracker
func recreateSimpleTestEnv() {
log.Infof("Creating simple test environment\n")
helmreconciler.TestMode = true
s := scheme.Scheme
s.AddKnownTypes(v1alpha1.SchemeGroupVersion, &v1alpha1.IstioOperator{})
testClient = fake.NewClientBuilder().WithScheme(s).Build()
testReconcileOperator = istiocontrolplane.NewReconcileIstioOperator(testClient, kube.NewFakeClient(), s)
}
// runManifestCommands runs all testedManifestCmds commands with the given input IOP file, flags and chartSource.
// It returns an ObjectSet for each cmd type.
// nolint: unparam
func runManifestCommands(inFile, flags string, chartSource chartSourceType, fileSelect []string) (map[cmdType]*ObjectSet, error) {
out := make(map[cmdType]*ObjectSet)
for _, cmd := range testedManifestCmds {
log.Infof("\nRunning test command using %s\n", cmd)
switch cmd {
case cmdApply, cmdController:
if err := cleanTestCluster(); err != nil {
return nil, err
}
if err := fakeApplyExtraResources(inFile); err != nil {
return nil, err
}
default:
}
var objs *ObjectSet
var err error
switch cmd {
case cmdGenerate:
m, _, err := generateManifest(inFile, flags, chartSource, fileSelect)
if err != nil {
return nil, err
}
objs, err = parseObjectSetFromManifest(m)
if err != nil {
return nil, err
}
case cmdApply:
objs, err = fakeApplyManifest(inFile, flags, chartSource)
case cmdController:
objs, err = fakeControllerReconcile(inFile, chartSource, nil)
default:
}
if err != nil {
return nil, err
}
out[cmd] = objs
}
return out, nil
}
// fakeApplyManifest runs istioctl install.
func fakeApplyManifest(inFile, flags string, chartSource chartSourceType) (*ObjectSet, error) {
inPath := filepath.Join(testDataDir, "input", inFile+".yaml")
manifest, err := runManifestCommand("install", []string{inPath}, flags, chartSource, nil)
if err != nil {
return nil, fmt.Errorf("error %s: %s", err, manifest)
}
return NewObjectSet(getAllIstioObjects()), nil
}
// fakeApplyExtraResources applies any extra resources for the given test name.
func fakeApplyExtraResources(inFile string) error {
reconciler, err := helmreconciler.NewHelmReconciler(testClient, nil, nil, nil)
if err != nil {
return err
}
if rs, err := readFile(filepath.Join(testDataDir, "input-extra-resources", inFile+".yaml")); err == nil {
if err := applyWithReconciler(reconciler, rs); err != nil {
return err
}
}
return nil
}
func fakeControllerReconcile(inFile string, chartSource chartSourceType, opts *helmreconciler.Options) (*ObjectSet, error) {
c := kube.NewFakeClientWithVersion("25")
l := clog.NewDefaultLogger()
_, iop, err := manifest.GenerateConfig(
[]string{inFileAbsolutePath(inFile)},
[]string{"installPackagePath=" + string(chartSource)},
false, c, l)
if err != nil {
return nil, err
}
iop.Spec.InstallPackagePath = string(chartSource)
reconciler, err := helmreconciler.NewHelmReconciler(testClient, c, iop, opts)
if err != nil {
return nil, err
}
if err := fakeInstallOperator(reconciler, chartSource, iop); err != nil {
return nil, err
}
if _, err := reconciler.Reconcile(); err != nil {
return nil, err
}
return NewObjectSet(getAllIstioObjects()), nil
}
// fakeInstallOperator installs the operator manifest resources into a cluster using the given reconciler.
// The installation is for testing with a kubebuilder fake cluster only, since no functional Deployment will be
// created.
func fakeInstallOperator(reconciler *helmreconciler.HelmReconciler, chartSource chartSourceType, iop *v1alpha1.IstioOperator) error {
ocArgs := &operatorCommonArgs{
manifestsPath: string(chartSource),
istioNamespace: istioDefaultNamespace,
watchedNamespaces: istioDefaultNamespace,
operatorNamespace: operatorDefaultNamespace,
// placeholders, since the fake API server does not actually pull images and create pods.
hub: "fake hub",
tag: "fake tag",
}
_, mstr, err := renderOperatorManifest(nil, ocArgs)
if err != nil {
return err
}
if err := applyWithReconciler(reconciler, mstr); err != nil {
return err
}
iopStr, err := yaml.Marshal(iop)
if err != nil {
return err
}
if err := saveIOPToCluster(reconciler, string(iopStr)); err != nil {
return err
}
return err
}
// applyWithReconciler applies the given manifest string using the given reconciler.
func applyWithReconciler(reconciler *helmreconciler.HelmReconciler, manifest string) error {
m := name.Manifest{
// Name is not important here, only Content will be applied.
Name: name.IstioOperatorComponentName,
Content: manifest,
}
_, _, err := reconciler.ApplyManifest(m, false)
return err
}
// runManifestCommand runs the given manifest command. If filenames is set, passes the given filenames as -f flag,
// flags is passed to the command verbatim. If you set both flags and path, make sure to not use -f in flags.
func runManifestCommand(command string, filenames []string, flags string, chartSource chartSourceType, fileSelect []string) (string, error) {
var args string
if command == "install" {
args = "install"
} else {
args = "manifest " + command
}
for _, f := range filenames {
args += " -f " + f
}
if flags != "" {
args += " " + flags
}
if fileSelect != nil {
filters := []string{}
filters = append(filters, fileSelect...)
// Everything needs these
filters = append(filters, "templates/_affinity.tpl")
args += " --filter " + strings.Join(filters, ",")
}
args += " --set installPackagePath=" + string(chartSource)
return runCommand(args)
}
// runCommand runs the given command string.
func runCommand(command string) (string, error) {
var out bytes.Buffer
rootCmd := GetRootCmd(strings.Split(command, " "))
rootCmd.SetOut(&out)
err := rootCmd.Execute()
return out.String(), err
}
// cleanTestCluster resets the test cluster.
func cleanTestCluster() error {
// Needed in case we are running a test through this path that doesn't start a new process.
cache.FlushObjectCaches()
if !kubeBuilderInstalled() {
return nil
}
return recreateTestEnv()
}
// getAllIstioObjects lists all Istio GVK resources from the testClient.
func getAllIstioObjects() object.K8sObjects {
var out object.K8sObjects
for _, gvk := range append(allClusterGVKs, allNamespacedGVKs...) {
objects := &unstructured.UnstructuredList{}
objects.SetGroupVersionKind(gvk)
if err := testClient.List(context.TODO(), objects); err != nil {
log.Error(err.Error())
continue
}
for _, o := range objects.Items {
no := o.DeepCopy()
out = append(out, object.NewK8sObject(no, nil, nil))
}
}
return out
}
// readFile reads a file and returns the contents.
func readFile(path string) (string, error) {
b, err := os.ReadFile(path)
return string(b), err
}
// writeFile writes a file and returns an error if operation is unsuccessful.
func writeFile(path string, data []byte) error {
return os.WriteFile(path, data, 0o644)
}
// removeFile removes given file from provided path.
func removeFile(path string) error {
return os.Remove(path)
}
// inFileAbsolutePath returns the absolute path for an input file like "gateways".
func inFileAbsolutePath(inFile string) string {
return filepath.Join(testDataDir, "input", inFile+".yaml")
}