istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/components/namespace/kube.go (about)

     1  //  Copyright Istio Authors
     2  //
     3  //  Licensed under the Apache License, Version 2.0 (the "License");
     4  //  you may not use this file except in compliance with the License.
     5  //  You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  //  Unless required by applicable law or agreed to in writing, software
    10  //  distributed under the License is distributed on an "AS IS" BASIS,
    11  //  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  //  See the License for the specific language governing permissions and
    13  //  limitations under the License.
    14  
    15  package namespace
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"io"
    21  	"math/rand"
    22  	"strings"
    23  	"sync"
    24  	"time"
    25  
    26  	"github.com/google/go-cmp/cmp"
    27  	"github.com/hashicorp/go-multierror"
    28  	corev1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/types"
    31  
    32  	"istio.io/api/label"
    33  	"istio.io/istio/pkg/log"
    34  	"istio.io/istio/pkg/test/framework/components/cluster"
    35  	"istio.io/istio/pkg/test/framework/resource"
    36  	kube2 "istio.io/istio/pkg/test/kube"
    37  	"istio.io/istio/pkg/test/scopes"
    38  	"istio.io/istio/pkg/test/util/retry"
    39  )
    40  
    41  // nolint: gosec
    42  // Test only code
    43  var (
    44  	idctr int64
    45  	rnd   = rand.New(rand.NewSource(time.Now().UnixNano()))
    46  	mu    sync.Mutex
    47  )
    48  
    49  // kubeNamespace represents a Kubernetes namespace. It is tracked as a resource.
    50  type kubeNamespace struct {
    51  	ctx          resource.Context
    52  	id           resource.ID
    53  	name         string
    54  	prefix       string
    55  	cleanupMutex sync.Mutex
    56  	cleanupFuncs []func() error
    57  	skipDump     bool
    58  }
    59  
    60  func (n *kubeNamespace) Dump(ctx resource.Context) {
    61  	if n.skipDump {
    62  		scopes.Framework.Debugf("=== Skip dumping Namespace %s State for %v...", n.name, ctx.ID())
    63  		return
    64  	}
    65  	scopes.Framework.Errorf("=== Dumping Namespace %s State for %v...", n.name, ctx.ID())
    66  
    67  	d, err := ctx.CreateTmpDirectory(n.name + "-state")
    68  	if err != nil {
    69  		scopes.Framework.Errorf("Unable to create directory for dumping %s contents: %v", n.name, err)
    70  		return
    71  	}
    72  
    73  	kube2.DumpPods(n.ctx, d, n.name, []string{})
    74  	kube2.DumpDeployments(n.ctx, d, n.name)
    75  }
    76  
    77  var (
    78  	_ Instance          = &kubeNamespace{}
    79  	_ io.Closer         = &kubeNamespace{}
    80  	_ resource.Resource = &kubeNamespace{}
    81  	_ resource.Dumper   = &kubeNamespace{}
    82  )
    83  
    84  func (n *kubeNamespace) Name() string {
    85  	return n.name
    86  }
    87  
    88  func (n *kubeNamespace) Prefix() string {
    89  	return n.prefix
    90  }
    91  
    92  func (n *kubeNamespace) Labels() (map[string]string, error) {
    93  	perCluster := make([]map[string]string, len(n.ctx.AllClusters().Kube()))
    94  	if err := n.forEachCluster(func(i int, c cluster.Cluster) error {
    95  		ns, err := c.Kube().CoreV1().Namespaces().Get(context.TODO(), n.Name(), metav1.GetOptions{})
    96  		if err != nil {
    97  			return err
    98  		}
    99  		perCluster[i] = ns.Labels
   100  		return nil
   101  	}); err != nil {
   102  		return nil, err
   103  	}
   104  	for i, clusterLabels := range perCluster {
   105  		if i == 0 {
   106  			continue
   107  		}
   108  		if diff := cmp.Diff(perCluster[0], clusterLabels); diff != "" {
   109  			log.Warnf("namespace labels are different across clusters:\n%s", diff)
   110  		}
   111  	}
   112  	return perCluster[0], nil
   113  }
   114  
   115  func (n *kubeNamespace) SetLabel(key, value string) error {
   116  	return n.setNamespaceLabel(key, value)
   117  }
   118  
   119  func (n *kubeNamespace) RemoveLabel(key string) error {
   120  	return n.removeNamespaceLabel(key)
   121  }
   122  
   123  func (n *kubeNamespace) ID() resource.ID {
   124  	return n.id
   125  }
   126  
   127  func (n *kubeNamespace) Close() error {
   128  	// Get the cleanup funcs and clear the array to prevent us from cleaning up multiple times.
   129  	n.cleanupMutex.Lock()
   130  	cleanupFuncs := n.cleanupFuncs
   131  	n.cleanupFuncs = nil
   132  	n.cleanupMutex.Unlock()
   133  
   134  	// Perform the cleanup across all clusters concurrently.
   135  	var err error
   136  	if len(cleanupFuncs) > 0 {
   137  		scopes.Framework.Debugf("%s deleting namespace %v", n.id, n.name)
   138  
   139  		g := multierror.Group{}
   140  		for _, cleanup := range cleanupFuncs {
   141  			g.Go(cleanup)
   142  		}
   143  
   144  		err = g.Wait().ErrorOrNil()
   145  	}
   146  
   147  	scopes.Framework.Debugf("%s close complete (err:%v)", n.id, err)
   148  	return err
   149  }
   150  
   151  func claimKube(ctx resource.Context, cfg Config) (Instance, error) {
   152  	name := cfg.Prefix
   153  	n := &kubeNamespace{
   154  		ctx:      ctx,
   155  		prefix:   name,
   156  		name:     name,
   157  		skipDump: cfg.SkipDump,
   158  	}
   159  
   160  	id := ctx.TrackResource(n)
   161  	n.id = id
   162  
   163  	if err := n.forEachCluster(func(_ int, c cluster.Cluster) error {
   164  		if !kube2.NamespaceExists(c.Kube(), name) {
   165  			return n.createInCluster(c, cfg)
   166  		}
   167  		return nil
   168  	}); err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	return n, nil
   173  }
   174  
   175  // setNamespaceLabel labels a namespace with the given key, value pair
   176  func (n *kubeNamespace) setNamespaceLabel(key, value string) error {
   177  	// need to convert '/' to '~1' as per the JSON patch spec http://jsonpatch.com/#operations
   178  	jsonPatchEscapedKey := strings.ReplaceAll(key, "/", "~1")
   179  	nsLabelPatch := fmt.Sprintf(`[{"op":"replace","path":"/metadata/labels/%s","value":"%s"}]`, jsonPatchEscapedKey, value)
   180  
   181  	return n.forEachCluster(func(_ int, c cluster.Cluster) error {
   182  		_, err := c.Kube().CoreV1().Namespaces().Patch(context.TODO(), n.name, types.JSONPatchType, []byte(nsLabelPatch), metav1.PatchOptions{})
   183  		return err
   184  	})
   185  }
   186  
   187  // removeNamespaceLabel removes namespace label with the given key
   188  func (n *kubeNamespace) removeNamespaceLabel(key string) error {
   189  	// need to convert '/' to '~1' as per the JSON patch spec http://jsonpatch.com/#operations
   190  	jsonPatchEscapedKey := strings.ReplaceAll(key, "/", "~1")
   191  	nsLabelPatch := fmt.Sprintf(`[{"op":"remove","path":"/metadata/labels/%s"}]`, jsonPatchEscapedKey)
   192  	name := n.name
   193  
   194  	return n.forEachCluster(func(_ int, c cluster.Cluster) error {
   195  		_, err := c.Kube().CoreV1().Namespaces().Patch(context.TODO(), name, types.JSONPatchType, []byte(nsLabelPatch), metav1.PatchOptions{})
   196  		return err
   197  	})
   198  }
   199  
   200  // NewNamespace allocates a new testing namespace.
   201  func newKube(ctx resource.Context, cfg Config) (Instance, error) {
   202  	mu.Lock()
   203  	idctr++
   204  	nsid := idctr
   205  	r := rnd.Intn(99999)
   206  	mu.Unlock()
   207  
   208  	name := fmt.Sprintf("%s-%d-%d", cfg.Prefix, nsid, r)
   209  	n := &kubeNamespace{
   210  		name:   name,
   211  		prefix: cfg.Prefix,
   212  		ctx:    ctx,
   213  	}
   214  	id := ctx.TrackResource(n)
   215  	n.id = id
   216  
   217  	if err := n.forEachCluster(func(_ int, c cluster.Cluster) error {
   218  		return n.createInCluster(c, cfg)
   219  	}); err != nil {
   220  		return nil, err
   221  	}
   222  
   223  	return n, nil
   224  }
   225  
   226  func (n *kubeNamespace) createInCluster(c cluster.Cluster, cfg Config) error {
   227  	if _, err := c.Kube().CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{
   228  		ObjectMeta: metav1.ObjectMeta{
   229  			Name:   n.name,
   230  			Labels: createNamespaceLabels(n.ctx, cfg),
   231  		},
   232  	}, metav1.CreateOptions{}); err != nil {
   233  		return err
   234  	}
   235  
   236  	if !cfg.SkipCleanup {
   237  		n.addCleanup(func() error {
   238  			return c.Kube().CoreV1().Namespaces().Delete(context.TODO(), n.name, kube2.DeleteOptionsForeground())
   239  		})
   240  	}
   241  
   242  	s := n.ctx.Settings()
   243  	if s.Image.PullSecret != "" {
   244  		if err := c.ApplyYAMLFiles(n.name, s.Image.PullSecret); err != nil {
   245  			return err
   246  		}
   247  		err := retry.UntilSuccess(func() error {
   248  			_, err := c.Kube().CoreV1().ServiceAccounts(n.name).Patch(context.TODO(),
   249  				"default",
   250  				types.JSONPatchType,
   251  				[]byte(`[{"op": "add", "path": "/imagePullSecrets", "value": [{"name": "test-gcr-secret"}]}]`),
   252  				metav1.PatchOptions{})
   253  			return err
   254  		}, retry.Delay(1*time.Second), retry.Timeout(10*time.Second))
   255  		if err != nil {
   256  			return err
   257  		}
   258  	}
   259  	return nil
   260  }
   261  
   262  func (n *kubeNamespace) forEachCluster(fn func(i int, c cluster.Cluster) error) error {
   263  	errG := multierror.Group{}
   264  	for i, c := range n.ctx.AllClusters().Kube() {
   265  		i, c := i, c
   266  		errG.Go(func() error {
   267  			return fn(i, c)
   268  		})
   269  	}
   270  	return errG.Wait().ErrorOrNil()
   271  }
   272  
   273  func (n *kubeNamespace) addCleanup(fn func() error) {
   274  	n.cleanupMutex.Lock()
   275  	defer n.cleanupMutex.Unlock()
   276  	n.cleanupFuncs = append(n.cleanupFuncs, fn)
   277  }
   278  
   279  func (n *kubeNamespace) IsAmbient() bool {
   280  	// TODO cache labels and invalidate on SetLabel to avoid a ton of kube calls
   281  	labels, err := n.Labels()
   282  	if err != nil {
   283  		scopes.Framework.Warnf("failed getting labels for namespace %s, assuming ambient is on", n.name)
   284  	}
   285  	return err != nil || labels["istio.io/dataplane-mode"] == "ambient"
   286  }
   287  
   288  func (n *kubeNamespace) IsInjected() bool {
   289  	if n == nil {
   290  		return false
   291  	}
   292  	// TODO cache labels and invalidate on SetLabel to avoid a ton of kube calls
   293  	labels, err := n.Labels()
   294  	if err != nil {
   295  		scopes.Framework.Warnf("failed getting labels for namespace %s, assuming injection is on", n.name)
   296  		return true
   297  	}
   298  	_, hasRevision := labels[label.IoIstioRev.Name]
   299  	return hasRevision || labels["istio-injection"] == "enabled"
   300  }
   301  
   302  // createNamespaceLabels will take a namespace config and generate the proper k8s labels
   303  func createNamespaceLabels(ctx resource.Context, cfg Config) map[string]string {
   304  	l := make(map[string]string)
   305  	l["istio-testing"] = "istio-test"
   306  	if cfg.Inject {
   307  		// do not add namespace labels when running compatibility tests since
   308  		// this disables the necessary object selectors
   309  		if !ctx.Settings().Compatibility {
   310  			if cfg.Revision != "" {
   311  				l[label.IoIstioRev.Name] = cfg.Revision
   312  			} else {
   313  				l["istio-injection"] = "enabled"
   314  			}
   315  		}
   316  	} else {
   317  		// if we're running compatibility tests, disable injection in the namespace
   318  		// explicitly so that object selectors are ignored
   319  		if ctx.Settings().Compatibility {
   320  			l["istio-injection"] = "disabled"
   321  		}
   322  	}
   323  
   324  	// bring over supplied labels
   325  	for k, v := range cfg.Labels {
   326  		l[k] = v
   327  	}
   328  	return l
   329  }