blob: 2baeaeb353277a78eda8614fe5698e8a0762f0c8 [file] [log] [blame]
//go:build integ
// +build integ
// 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 operator
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"
"time"
)
import (
"github.com/golang/protobuf/jsonpb" // nolint: staticcheck
"github.com/hashicorp/go-multierror"
api "istio.io/api/operator/v1alpha1"
"istio.io/pkg/log"
kubeApiCore "k8s.io/api/core/v1"
kubeApiMeta "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
import (
"github.com/apache/dubbo-go-pixiu/operator/pkg/object"
"github.com/apache/dubbo-go-pixiu/operator/pkg/util"
istioKube "github.com/apache/dubbo-go-pixiu/pkg/kube"
"github.com/apache/dubbo-go-pixiu/pkg/test/env"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/cluster"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework/components/istioctl"
"github.com/apache/dubbo-go-pixiu/pkg/test/framework/resource"
kube2 "github.com/apache/dubbo-go-pixiu/pkg/test/kube"
"github.com/apache/dubbo-go-pixiu/pkg/test/scopes"
"github.com/apache/dubbo-go-pixiu/pkg/test/util/retry"
"github.com/apache/dubbo-go-pixiu/tests/util/sanitycheck"
)
const (
IstioNamespace = "dubbo-system"
OperatorNamespace = "istio-operator"
retryDelay = time.Second
retryTimeOut = 20 * time.Minute
nsDeletionTimeout = 5 * time.Minute
)
var (
// ManifestPath is path of local manifests which istioctl operator init refers to.
ManifestPath = filepath.Join(env.IstioSrc, "manifests")
// ManifestPathContainer is path of manifests in the operator container for controller to work with.
ManifestPathContainer = "/var/lib/istio/manifests"
iopCRFile = ""
)
func TestController(t *testing.T) {
framework.
NewTest(t).
Run(func(t framework.TestContext) {
istioCtl := istioctl.NewOrFail(t, t, istioctl.Config{})
workDir, err := t.CreateTmpDirectory("operator-controller-test")
if err != nil {
t.Fatal("failed to create test directory")
}
cs := t.Clusters().Default()
cleanupInClusterCRs(t, cs)
t.Cleanup(func() {
cleanupIstioResources(t, cs, istioCtl)
})
s := t.Settings()
initCmd := []string{
"operator", "init",
"--hub=" + s.Image.Hub,
"--tag=" + s.Image.Tag,
"--manifests=" + ManifestPath,
}
// install istio with default config for the first time by running operator init command
istioCtl.InvokeOrFail(t, initCmd)
t.TrackResource(&operatorDumper{rev: ""})
if _, err := cs.CoreV1().Namespaces().Create(context.TODO(), &kubeApiCore.Namespace{
ObjectMeta: kubeApiMeta.ObjectMeta{
Name: IstioNamespace,
},
}, kubeApiMeta.CreateOptions{}); err != nil {
_, err := cs.CoreV1().Namespaces().Get(context.TODO(), IstioNamespace, kubeApiMeta.GetOptions{})
if err == nil {
log.Info("istio namespace already exist")
} else {
t.Errorf("failed to create istio namespace: %v", err)
}
}
iopCRFile = filepath.Join(workDir, "iop_cr.yaml")
// later just run `kubectl apply -f newcr.yaml` to apply new installation cr files and verify.
installWithCRFile(t, t, cs, istioCtl, "demo", "")
initCmd = []string{
"operator", "init",
"--hub=" + s.Image.Hub,
"--tag=" + s.Image.Tag,
"--manifests=" + ManifestPath,
"--revision=" + "v2",
}
// install second operator deployment with different revision
istioCtl.InvokeOrFail(t, initCmd)
t.TrackResource(&operatorDumper{rev: "v2"})
installWithCRFile(t, t, cs, istioCtl, "default", "v2")
installWithCRFile(t, t, cs, istioCtl, "default", "")
// istio control plane resources expected to be deleted after deleting CRs
cleanupInClusterCRs(t, cs)
// test operator remove command
scopes.Framework.Infof("checking operator remove command")
removeCmd := []string{
"operator", "remove",
}
istioCtl.InvokeOrFail(t, removeCmd)
retry.UntilSuccessOrFail(t, func() error {
for _, n := range []string{"istio-operator", "istio-operator-v2"} {
if svc, _ := cs.CoreV1().Services(OperatorNamespace).Get(context.TODO(), n, kubeApiMeta.GetOptions{}); svc.Name != "" {
return fmt.Errorf("got operator service: %s from cluster, expected to be removed", svc.Name)
}
if dp, _ := cs.AppsV1().Deployments(OperatorNamespace).Get(context.TODO(), n, kubeApiMeta.GetOptions{}); dp.Name != "" {
return fmt.Errorf("got operator deployment %s from cluster, expected to be removed", dp.Name)
}
}
return nil
}, retry.Timeout(retryTimeOut), retry.Delay(retryDelay))
})
}
func cleanupIstioResources(t framework.TestContext, cs cluster.Cluster, istioCtl istioctl.Instance) {
scopes.Framework.Infof("cleaning up resources")
// clean up Istio control plane
unInstallCmd := []string{
"x", "uninstall", "--purge", "--skip-confirmation",
}
out, _ := istioCtl.InvokeOrFail(t, unInstallCmd)
t.Logf("uninstall command output: %s", out)
// clean up operator namespace
if err := cs.CoreV1().Namespaces().Delete(context.TODO(), OperatorNamespace,
kube2.DeleteOptionsForeground()); err != nil {
t.Logf("failed to delete operator namespace: %v", err)
}
if err := kube2.WaitForNamespaceDeletion(cs, OperatorNamespace, retry.Timeout(nsDeletionTimeout)); err != nil {
t.Logf("failed waiting for operator namespace to be deleted: %v", err)
}
var err error
// clean up dynamically created secret and configmaps
if e := cs.CoreV1().Secrets(IstioNamespace).DeleteCollection(
context.Background(), kubeApiMeta.DeleteOptions{}, kubeApiMeta.ListOptions{}); e != nil {
err = multierror.Append(err, e)
}
if e := cs.CoreV1().ConfigMaps(IstioNamespace).DeleteCollection(
context.Background(), kubeApiMeta.DeleteOptions{}, kubeApiMeta.ListOptions{}); e != nil {
err = multierror.Append(err, e)
}
if err != nil {
scopes.Framework.Errorf("failed to cleanup dynamically created resources: %v", err)
}
}
// checkInstallStatus check the status of IstioOperator CR from the cluster
func checkInstallStatus(cs istioKube.ExtendedClient, revision string) error {
scopes.Framework.Infof("checking IstioOperator CR status")
gvr := schema.GroupVersionResource{
Group: "install.istio.io",
Version: "v1alpha1",
Resource: "istiooperators",
}
var unhealthyCN []string
retryFunc := func() error {
us, err := cs.Dynamic().Resource(gvr).Namespace(IstioNamespace).Get(context.TODO(), revName("test-istiocontrolplane", revision), kubeApiMeta.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get istioOperator resource: %v", err)
}
usIOPStatus := us.UnstructuredContent()["status"]
if usIOPStatus == nil {
if _, err := cs.CoreV1().Services(OperatorNamespace).Get(context.TODO(), revName("istio-operator", revision),
kubeApiMeta.GetOptions{}); err != nil {
return fmt.Errorf("istio operator svc is not ready: %v", err)
}
if _, err := kube2.CheckPodsAreReady(kube2.NewPodFetch(cs, OperatorNamespace, "")); err != nil {
return fmt.Errorf("istio operator pod is not ready: %v", err)
}
return fmt.Errorf("status not found from the istioOperator resource")
}
usIOPStatus = usIOPStatus.(map[string]interface{})
iopStatusString, err := json.Marshal(usIOPStatus)
if err != nil {
return fmt.Errorf("failed to marshal istioOperator status: %v", err)
}
status := &api.InstallStatus{}
jspb := jsonpb.Unmarshaler{AllowUnknownFields: true}
if err := jspb.Unmarshal(bytes.NewReader(iopStatusString), status); err != nil {
return fmt.Errorf("failed to unmarshal istioOperator status: %v", err)
}
errs := util.Errors{}
unhealthyCN = []string{}
if status.Status != api.InstallStatus_HEALTHY {
errs = util.AppendErr(errs, fmt.Errorf("got IstioOperator status: %v", status.Status))
}
for cn, cnstatus := range status.ComponentStatus {
if cnstatus.Status != api.InstallStatus_HEALTHY {
unhealthyCN = append(unhealthyCN, cn)
errs = util.AppendErr(errs, fmt.Errorf("got component: %s status: %v", cn, cnstatus.Status))
}
}
return errs.ToError()
}
scopes.Framework.Infof("waiting for IOP to become healthy")
err := retry.UntilSuccess(retryFunc, retry.Timeout(retryTimeOut), retry.Delay(retryDelay))
if err != nil {
return fmt.Errorf("istioOperator status is not healthy: %v", err)
}
return nil
}
func cleanupInClusterCRs(t framework.TestContext, cs cluster.Cluster) {
// clean up hanging installed-state CR from previous tests, failing for errors is not needed here.
scopes.Framework.Info("cleaning up in-cluster CRs")
gvr := schema.GroupVersionResource{
Group: "install.istio.io",
Version: "v1alpha1",
Resource: "istiooperators",
}
crList, err := cs.Dynamic().Resource(gvr).Namespace(IstioNamespace).List(context.TODO(),
kubeApiMeta.ListOptions{})
if err == nil {
for _, obj := range crList.Items {
t.Logf("deleting CR %v", obj.GetName())
if err := cs.Dynamic().Resource(gvr).Namespace(IstioNamespace).Delete(context.TODO(), obj.GetName(),
kubeApiMeta.DeleteOptions{}); err != nil {
t.Logf("failed to delete existing CR: %v", err)
}
}
} else {
t.Logf("failed to list existing CR: %v", err.Error())
}
scopes.Framework.Infof("waiting for pods in dubbo-system to be deleted")
// wait for pods in dubbo-system to be deleted
err = retry.UntilSuccess(func() error {
podList, err := cs.Kube().CoreV1().Pods(IstioNamespace).List(context.TODO(), kubeApiMeta.ListOptions{})
if err != nil {
return err
}
if len(podList.Items) == 0 {
return nil
}
names := []string{}
for _, i := range podList.Items {
names = append(names, i.Name)
}
return fmt.Errorf("pods still remain in %s: %v", IstioNamespace, names)
}, retry.Timeout(retryTimeOut), retry.Delay(retryDelay))
if err != nil {
t.Logf("failed to delete pods in %s: %v", IstioNamespace, err)
} else {
t.Logf("all pods in dubbo-system deleted")
}
}
func installWithCRFile(t framework.TestContext, ctx resource.Context, cs cluster.Cluster,
istioCtl istioctl.Instance, profileName string, revision string) {
scopes.Framework.Infof(fmt.Sprintf("=== install istio with profile: %s===\n", profileName))
metadataYAML := `
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
name: %s
namespace: dubbo-system
spec:
`
if revision != "" {
metadataYAML += " revision: " + revision + "\n"
}
metadataYAML += `
profile: %s
installPackagePath: %s
hub: %s
tag: %s
values:
global:
imagePullPolicy: %s
`
s := ctx.Settings()
overlayYAML := fmt.Sprintf(metadataYAML, revName("test-istiocontrolplane", revision), profileName, ManifestPathContainer,
s.Image.Hub, s.Image.Tag, s.Image.PullPolicy)
scopes.Framework.Infof("=== installing with IOP: ===\n%s\n", overlayYAML)
if err := os.WriteFile(iopCRFile, []byte(overlayYAML), os.ModePerm); err != nil {
t.Fatalf("failed to write iop cr file: %v", err)
}
if err := cs.ApplyYAMLFiles(IstioNamespace, iopCRFile); err != nil {
t.Fatalf("failed to apply IstioOperator CR file: %s, %v", iopCRFile, err)
}
verifyInstallation(t, ctx, istioCtl, profileName, revision, cs)
}
// verifyInstallation verify IOP CR status and compare in-cluster resources with generated ones.
// It also returns the expected K8sObjects generated by manifest generate command.
func verifyInstallation(t framework.TestContext, ctx resource.Context,
istioCtl istioctl.Instance, profileName string, revision string, cs cluster.Cluster) object.K8sObjects {
scopes.Framework.Infof("=== verifying istio installation revision %s === ", revision)
if err := checkInstallStatus(cs, revision); err != nil {
t.Fatalf("IstioOperator status not healthy: %v", err)
}
if _, err := kube2.CheckPodsAreReady(kube2.NewSinglePodFetch(cs, IstioNamespace, "app=istiod")); err != nil {
t.Fatalf("istiod pod is not ready: %v", err)
}
// get manifests by running `manifest generate`
generateCmd := []string{
"manifest", "generate",
"--manifests", ManifestPath,
}
if profileName != "" {
generateCmd = append(generateCmd, "--set", fmt.Sprintf("profile=%s", profileName))
}
if revision != "" {
generateCmd = append(generateCmd, "--revision", revision)
}
genManifests, _ := istioCtl.InvokeOrFail(t, generateCmd)
K8SObjects, err := object.ParseK8sObjectsFromYAMLManifest(genManifests)
if err != nil {
t.Errorf("failed to parse generated manifest: %v", err)
}
compareInClusterAndGeneratedResources(t, cs, K8SObjects, false)
sanitycheck.RunTrafficTest(t, ctx)
scopes.Framework.Infof("=== succeeded ===")
return K8SObjects
}
func compareInClusterAndGeneratedResources(t framework.TestContext, cs cluster.Cluster, k8sObjects object.K8sObjects,
expectRemoved bool) {
// nolint: staticcheck
if k8sObjects == nil {
t.Fatalf("expected K8sObjects is nil")
}
efgvr := schema.GroupVersionResource{
Group: "networking.istio.io",
Version: "v1alpha3",
Resource: "envoyfilters",
}
// nolint: staticcheck
for _, genK8SObject := range k8sObjects {
kind := genK8SObject.Kind
ns := genK8SObject.Namespace
name := genK8SObject.Name
scopes.Framework.Infof("checking kind: %s, namespace: %s, name: %s", kind, ns, name)
retry.UntilSuccessOrFail(t, func() error {
var err error
switch kind {
case "Service":
_, err = cs.CoreV1().Services(ns).Get(context.TODO(), name, kubeApiMeta.GetOptions{})
case "ServiceAccount":
_, err = cs.CoreV1().ServiceAccounts(ns).Get(context.TODO(), name, kubeApiMeta.GetOptions{})
case "Deployment":
_, err = cs.AppsV1().Deployments(IstioNamespace).Get(context.TODO(), name,
kubeApiMeta.GetOptions{})
case "ConfigMap":
_, err = cs.CoreV1().ConfigMaps(ns).Get(context.TODO(), name, kubeApiMeta.GetOptions{})
case "ValidatingWebhookConfiguration":
_, err = cs.AdmissionregistrationV1().ValidatingWebhookConfigurations().Get(context.TODO(),
name, kubeApiMeta.GetOptions{})
case "MutatingWebhookConfiguration":
_, err = cs.AdmissionregistrationV1().MutatingWebhookConfigurations().Get(context.TODO(),
name, kubeApiMeta.GetOptions{})
case "CustomResourceDefinition":
_, err = cs.Ext().ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), name,
kubeApiMeta.GetOptions{})
case "EnvoyFilter":
_, err = cs.Dynamic().Resource(efgvr).Namespace(ns).Get(context.TODO(), name,
kubeApiMeta.GetOptions{})
case "PodDisruptionBudget":
// policy/v1 is available on >=1.21
if cs.MinKubeVersion(21) {
_, err = cs.PolicyV1().PodDisruptionBudgets(ns).Get(context.TODO(), name, kubeApiMeta.GetOptions{})
} else {
_, err = cs.PolicyV1beta1().PodDisruptionBudgets(ns).Get(context.TODO(), name, kubeApiMeta.GetOptions{})
}
case "HorizontalPodAutoscaler":
// autoscaling v2 API is available on >=1.23
if cs.MinKubeVersion(23) {
_, err = cs.AutoscalingV2().HorizontalPodAutoscalers(ns).Get(context.TODO(), name, kubeApiMeta.GetOptions{})
} else {
_, err = cs.AutoscalingV2beta2().HorizontalPodAutoscalers(ns).Get(context.TODO(), name, kubeApiMeta.GetOptions{})
}
}
if err != nil && !expectRemoved {
return fmt.Errorf("failed to get expected %s: %s from cluster", kind, name)
}
if err == nil && expectRemoved && kind != "CustomResourceDefinition" {
return fmt.Errorf("%s: %s expected to be removed from cluster but still exists", kind, name)
}
return nil
}, retry.Timeout(time.Second*300), retry.Delay(time.Millisecond*100))
}
}
func revName(name, revision string) string {
if revision == "" {
return name
}
return name + "-" + revision
}