sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/clusterclass_test.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package client
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"testing"
    23  
    24  	. "github.com/onsi/gomega"
    25  	"github.com/onsi/gomega/types"
    26  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	"k8s.io/apimachinery/pkg/runtime/schema"
    30  	"sigs.k8s.io/controller-runtime/pkg/client"
    31  
    32  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    33  	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster"
    34  	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository"
    35  	yaml "sigs.k8s.io/cluster-api/cmd/clusterctl/client/yamlprocessor"
    36  	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test"
    37  	utilyaml "sigs.k8s.io/cluster-api/util/yaml"
    38  )
    39  
    40  func TestClusterClassExists(t *testing.T) {
    41  	tests := []struct {
    42  		name         string
    43  		objs         []client.Object
    44  		clusterClass string
    45  		want         bool
    46  	}{
    47  		{
    48  			name: "should return true when checking for an installed cluster class",
    49  			objs: []client.Object{
    50  				&clusterv1.ClusterClass{
    51  					ObjectMeta: metav1.ObjectMeta{
    52  						Name:      "clusterclass-installed",
    53  						Namespace: metav1.NamespaceDefault,
    54  					},
    55  				},
    56  			},
    57  			clusterClass: "clusterclass-installed",
    58  			want:         true,
    59  		},
    60  		{
    61  			name:         "should return false when checking for a not-installed cluster class",
    62  			objs:         []client.Object{},
    63  			clusterClass: "clusterclass-not-installed",
    64  			want:         false,
    65  		},
    66  	}
    67  	for _, tt := range tests {
    68  		t.Run(tt.name, func(t *testing.T) {
    69  			g := NewWithT(t)
    70  
    71  			ctx := context.Background()
    72  
    73  			config := newFakeConfig(ctx)
    74  			client := newFakeCluster(cluster.Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, config).WithObjs(tt.objs...)
    75  			c, _ := client.Proxy().NewClient(ctx)
    76  
    77  			actual, err := clusterClassExists(ctx, c, tt.clusterClass, metav1.NamespaceDefault)
    78  			g.Expect(err).ToNot(HaveOccurred())
    79  			g.Expect(actual).To(Equal(tt.want))
    80  		})
    81  	}
    82  }
    83  
    84  func TestAddClusterClassIfMissing(t *testing.T) {
    85  	infraClusterTemplateNS4 := unstructured.Unstructured{}
    86  	infraClusterTemplateNS4.SetNamespace("ns4")
    87  	infraClusterTemplateNS4.SetGroupVersionKind(schema.GroupVersionKind{
    88  		Version: "v1",
    89  		Kind:    "InfrastructureClusterTemplate",
    90  	})
    91  	infraClusterTemplateNS4.SetName("testInfraClusterTemplate")
    92  	infraClusterTemplateNS4Bytes, err := utilyaml.FromUnstructured([]unstructured.Unstructured{infraClusterTemplateNS4})
    93  	if err != nil {
    94  		panic("failed to convert template to bytes")
    95  	}
    96  
    97  	tests := []struct {
    98  		name                        string
    99  		clusterInitialized          bool
   100  		objs                        []client.Object
   101  		clusterClassTemplateContent []byte
   102  		targetNamespace             string
   103  		listVariablesOnly           bool
   104  		wantClusterClassInTemplate  bool
   105  		wantError                   bool
   106  	}{
   107  		{
   108  			name:                        "should add the cluster class to the template if cluster is not initialized",
   109  			clusterInitialized:          false,
   110  			objs:                        []client.Object{},
   111  			targetNamespace:             "ns1",
   112  			clusterClassTemplateContent: clusterClassYAML("ns1", "dev"),
   113  			listVariablesOnly:           false,
   114  			wantClusterClassInTemplate:  true,
   115  			wantError:                   false,
   116  		},
   117  		{
   118  			name:                        "should add the cluster class to the template if cluster is initialized and cluster class is not installed",
   119  			clusterInitialized:          true,
   120  			objs:                        []client.Object{},
   121  			targetNamespace:             "ns2",
   122  			clusterClassTemplateContent: clusterClassYAML("ns2", "dev"),
   123  			listVariablesOnly:           false,
   124  			wantClusterClassInTemplate:  true,
   125  			wantError:                   false,
   126  		},
   127  		{
   128  			name:               "should NOT add the cluster class to the template if cluster is initialized and cluster class is installed",
   129  			clusterInitialized: true,
   130  			objs: []client.Object{&clusterv1.ClusterClass{
   131  				ObjectMeta: metav1.ObjectMeta{
   132  					Name:      "dev",
   133  					Namespace: "ns3",
   134  				},
   135  			}},
   136  			targetNamespace:             "ns3",
   137  			clusterClassTemplateContent: clusterClassYAML("ns3", "dev"),
   138  			listVariablesOnly:           false,
   139  			wantClusterClassInTemplate:  false,
   140  			wantError:                   false,
   141  		},
   142  		{
   143  			name:               "should throw error if the cluster is initialized and templates from the cluster class template already exist in the cluster",
   144  			clusterInitialized: true,
   145  			objs: []client.Object{
   146  				&infraClusterTemplateNS4,
   147  			},
   148  			clusterClassTemplateContent: utilyaml.JoinYaml(clusterClassYAML("ns4", "dev"), infraClusterTemplateNS4Bytes),
   149  			targetNamespace:             "ns4",
   150  			listVariablesOnly:           false,
   151  			wantClusterClassInTemplate:  false,
   152  			wantError:                   true,
   153  		},
   154  	}
   155  
   156  	for _, tt := range tests {
   157  		t.Run(tt.name, func(t *testing.T) {
   158  			ctx := context.Background()
   159  
   160  			config1 := newFakeConfig(ctx).WithProvider(infraProviderConfig)
   161  			repository1 := newFakeRepository(ctx, infraProviderConfig, config1).
   162  				WithPaths("root", "").
   163  				WithDefaultVersion("v1.0.0").
   164  				WithFile("v1.0.0", "clusterclass-dev.yaml", tt.clusterClassTemplateContent)
   165  			cluster := newFakeCluster(cluster.Kubeconfig{Path: "kubeconfig", Context: "mgt-cluster"}, config1).WithObjs(tt.objs...)
   166  
   167  			if tt.clusterInitialized {
   168  				cluster.WithObjs(&apiextensionsv1.CustomResourceDefinition{
   169  					TypeMeta: metav1.TypeMeta{
   170  						APIVersion: apiextensionsv1.SchemeGroupVersion.String(),
   171  						Kind:       "CustomResourceDefinition",
   172  					},
   173  					ObjectMeta: metav1.ObjectMeta{
   174  						Name: fmt.Sprintf("clusters.%s", clusterv1.GroupVersion.Group),
   175  						Labels: map[string]string{
   176  							clusterv1.GroupVersion.String(): "v1beta1",
   177  						},
   178  					},
   179  					Spec: apiextensionsv1.CustomResourceDefinitionSpec{
   180  						Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
   181  							{
   182  								Storage: true,
   183  								Name:    clusterv1.GroupVersion.Version,
   184  							},
   185  						},
   186  					},
   187  				})
   188  			}
   189  
   190  			clusterClassClient := repository1.ClusterClasses("v1.0.0")
   191  
   192  			clusterWithTopology := []byte(fmt.Sprintf("apiVersion: %s\n", clusterv1.GroupVersion.String()) +
   193  				"kind: Cluster\n" +
   194  				"metadata:\n" +
   195  				"  name: cluster-dev\n" +
   196  				fmt.Sprintf("  namespace: %s\n", tt.targetNamespace) +
   197  				"spec:\n" +
   198  				"  topology:\n" +
   199  				"    class: dev")
   200  
   201  			baseTemplate, err := repository.NewTemplate(repository.TemplateInput{
   202  				RawArtifact:           clusterWithTopology,
   203  				ConfigVariablesClient: test.NewFakeVariableClient(),
   204  				Processor:             yaml.NewSimpleProcessor(),
   205  				TargetNamespace:       tt.targetNamespace,
   206  				SkipTemplateProcess:   false,
   207  			})
   208  			if err != nil {
   209  				t.Fatalf("failed to create template %v", err)
   210  			}
   211  
   212  			g := NewWithT(t)
   213  			template, err := addClusterClassIfMissing(ctx, baseTemplate, clusterClassClient, cluster, tt.targetNamespace, tt.listVariablesOnly)
   214  			if tt.wantError {
   215  				g.Expect(err).To(HaveOccurred())
   216  			} else {
   217  				if tt.wantClusterClassInTemplate {
   218  					g.Expect(template.Objs()).To(ContainElement(MatchClusterClass("dev", tt.targetNamespace)))
   219  				} else {
   220  					g.Expect(template.Objs()).NotTo(ContainElement(MatchClusterClass("dev", tt.targetNamespace)))
   221  				}
   222  			}
   223  		})
   224  	}
   225  }
   226  
   227  func MatchClusterClass(name, namespace string) types.GomegaMatcher {
   228  	return &clusterClassMatcher{name, namespace}
   229  }
   230  
   231  type clusterClassMatcher struct {
   232  	name      string
   233  	namespace string
   234  }
   235  
   236  func (cm *clusterClassMatcher) Match(actual interface{}) (bool, error) {
   237  	uClass := actual.(unstructured.Unstructured)
   238  
   239  	// check name
   240  	name, ok, err := unstructured.NestedString(uClass.UnstructuredContent(), "metadata", "name")
   241  	if err != nil {
   242  		return false, err
   243  	}
   244  	if !ok {
   245  		return false, nil
   246  	}
   247  	if name != cm.name {
   248  		return false, nil
   249  	}
   250  
   251  	// check namespace
   252  	namespace, ok, err := unstructured.NestedString(uClass.UnstructuredContent(), "metadata", "namespace")
   253  	if err != nil {
   254  		return false, err
   255  	}
   256  	if !ok {
   257  		return false, nil
   258  	}
   259  	if namespace != cm.namespace {
   260  		return false, nil
   261  	}
   262  
   263  	return true, nil
   264  }
   265  
   266  func (cm *clusterClassMatcher) FailureMessage(_ interface{}) string {
   267  	return fmt.Sprintf("Expected ClusterClass of name %v in namespace %v to be present", cm.name, cm.namespace)
   268  }
   269  
   270  func (cm *clusterClassMatcher) NegatedFailureMessage(_ interface{}) string {
   271  	return fmt.Sprintf("Expected ClusterClass of name %v in namespace %v not to be present", cm.name, cm.namespace)
   272  }