diff --git a/pkg/models/gateway/gateway.go b/pkg/models/gateway/gateway.go index 07ebb72b2..24d64804b 100644 --- a/pkg/models/gateway/gateway.go +++ b/pkg/models/gateway/gateway.go @@ -40,10 +40,11 @@ import ( ) const ( + SidecarInject = "sidecar.istio.io/inject" gatewayPrefix = "kubesphere-router-" workingNamespace = "kubesphere-controls-system" globalGatewayname = gatewayPrefix + "kubesphere-system" - helmPatch = `{"metadata":{"annotations":{"meta.helm.sh/release-name":"%s-ingress","meta.helm.sh/release-namespace":"%s"},"labels":{"helm.sh/chart":"ingress-nginx-3.35.0","app.kubernetes.io/managed-by":"Helm","app":null,"component":null,"tier":null}}}` + helmPatch = `{"metadata":{"annotations":{"meta.helm.sh/release-name":"%s-ingress","meta.helm.sh/release-namespace":"%s"},"labels":{"helm.sh/chart":"ingress-nginx-3.35.0","app.kubernetes.io/managed-by":"Helm","app":null,"component":null,"tier":null}},"spec":{"selector":null}}` ) type GatewayOperator interface { @@ -121,12 +122,15 @@ func (c *gatewayOperator) getLegacyGateway(namespace string) *v1alpha1.Gateway { // create a fake Gateway object when legacy service exists if len(s.Items) > 0 { - return c.convert(namespace, &s.Items[0]) + d := &appsv1.Deployment{} + c.client.Get(context.TODO(), client.ObjectKeyFromObject(&s.Items[0]), d) + + return c.convert(namespace, &s.Items[0], d) } return nil } -func (c *gatewayOperator) convert(namespace string, svc *corev1.Service) *v1alpha1.Gateway { +func (c *gatewayOperator) convert(namespace string, svc *corev1.Service, deploy *appsv1.Deployment) *v1alpha1.Gateway { legacy := v1alpha1.Gateway{ TypeMeta: v1.TypeMeta{ Kind: "", @@ -147,8 +151,15 @@ func (c *gatewayOperator) convert(namespace string, svc *corev1.Service) *v1alph Annotations: svc.Annotations, Type: svc.Spec.Type, }, + Deployment: v1alpha1.DeploymentSpec{ + Replicas: deploy.Spec.Replicas, + }, }, } + if an, ok := deploy.Annotations[SidecarInject]; ok { + legacy.Spec.Deployment.Annotations = make(map[string]string) + legacy.Spec.Deployment.Annotations[SidecarInject] = an + } return &legacy } @@ -228,14 +239,25 @@ func (c *gatewayOperator) UpgradeGateway(namespace string) (*v1alpha1.Gateway, e return nil, fmt.Errorf("invalid operation, can't upgrade legacy gateway when working namespace changed") } + // Get legency gateway's config from configmap + cm := &corev1.ConfigMap{} + err := c.client.Get(context.TODO(), client.ObjectKey{Namespace: l.Namespace, Name: fmt.Sprintf("%s-nginx", l.Name)}, cm) + if err == nil { + l.Spec.Conroller.Config = cm.Data + defer func() { + c.client.Delete(context.TODO(), cm) + }() + } + // Delete old deployment, because it's not compatile with the deployment in the helm chart. + // We can't defer here, there's a potential race condition causing gateway operator fails. d := &appsv1.Deployment{ ObjectMeta: v1.ObjectMeta{ Namespace: l.Namespace, Name: l.Name, }, } - err := c.client.Delete(context.TODO(), d) + err = c.client.Delete(context.TODO(), d) if err != nil && !errors.IsNotFound(err) { return nil, err } @@ -259,14 +281,14 @@ func (c *gatewayOperator) UpgradeGateway(namespace string) (*v1alpha1.Gateway, e } func (c *gatewayOperator) ListGateways(query *query.Query) (*api.ListResult, error) { - applications := v1alpha1.GatewayList{} - err := c.cache.List(context.TODO(), &applications, &client.ListOptions{LabelSelector: query.Selector()}) + gateways := v1alpha1.GatewayList{} + err := c.cache.List(context.TODO(), &gateways, &client.ListOptions{LabelSelector: query.Selector()}) if err != nil { return nil, err } var result []runtime.Object - for i := range applications.Items { - result = append(result, &applications.Items[i]) + for i := range gateways.Items { + result = append(result, &gateways.Items[i]) } services := &corev1.ServiceList{} @@ -282,29 +304,40 @@ func (c *gatewayOperator) ListGateways(query *query.Query) (*api.ListResult, err }) for _, s := range services.Items { - g := c.convert(s.Labels["project"], &s) - result = append(result, g) + result = append(result, &s) } - return v1alpha3.DefaultList(result, query, c.compare, c.filter), nil + return v1alpha3.DefaultList(result, query, c.compare, c.filter, c.transform), nil } -func (d *gatewayOperator) compare(left runtime.Object, right runtime.Object, field query.Field) bool { +func (c *gatewayOperator) transform(obj runtime.Object) runtime.Object { + if g, ok := obj.(*v1alpha1.Gateway); ok { + return g + } + if s, ok := obj.(*corev1.Service); ok { + d := &appsv1.Deployment{} + c.client.Get(context.TODO(), client.ObjectKeyFromObject(s), d) + return c.convert(s.Labels["project"], s, d) + } + return nil +} - leftApplication, ok := left.(*v1alpha1.Gateway) +func (c *gatewayOperator) compare(left runtime.Object, right runtime.Object, field query.Field) bool { + + leftGateway, ok := left.(*v1alpha1.Gateway) if !ok { return false } - rightApplication, ok := right.(*v1alpha1.Gateway) + rightGateway, ok := right.(*v1alpha1.Gateway) if !ok { return false } - return v1alpha3.DefaultObjectMetaCompare(leftApplication.ObjectMeta, rightApplication.ObjectMeta, field) + return v1alpha3.DefaultObjectMetaCompare(leftGateway.ObjectMeta, rightGateway.ObjectMeta, field) } -func (d *gatewayOperator) filter(object runtime.Object, filter query.Filter) bool { +func (c *gatewayOperator) filter(object runtime.Object, filter query.Filter) bool { gateway, ok := object.(*v1alpha1.Gateway) if !ok { return false diff --git a/pkg/models/gateway/gateway_test.go b/pkg/models/gateway/gateway_test.go index 170f811a1..5e2e170fb 100644 --- a/pkg/models/gateway/gateway_test.go +++ b/pkg/models/gateway/gateway_test.go @@ -26,11 +26,15 @@ import ( corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/diff" "kubesphere.io/api/gateway/v1alpha1" + "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "kubesphere.io/kubesphere/pkg/api" + "kubesphere.io/kubesphere/pkg/apiserver/query" "kubesphere.io/kubesphere/pkg/simple/client/gateway" ) @@ -239,6 +243,45 @@ func create_LegacyGateway(c client.Client, namespace string) { }, } c.Create(context.TODO(), s) + + d := &appsv1.Deployment{ + ObjectMeta: v1.ObjectMeta{ + Name: fmt.Sprint(gatewayPrefix, namespace), + Namespace: workingNamespace, + Annotations: map[string]string{ + SidecarInject: "true", + }, + Labels: map[string]string{ + "app": "kubesphere", + "component": "ks-router", + "tier": "backend", + "project": namespace, + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &[]int32{1}[0], + }, + } + c.Create(context.TODO(), d) +} + +func create_LegacyGatewayConfigMap(c client.Client, namespace string) { + s := &corev1.ConfigMap{ + ObjectMeta: v1.ObjectMeta{ + Name: fmt.Sprint(gatewayPrefix, namespace, "-nginx"), + Namespace: workingNamespace, + Labels: map[string]string{ + "app": "kubesphere", + "component": "ks-router", + "tier": "backend", + "project": namespace, + }, + }, + Data: map[string]string{ + "fake": "true", + }, + } + c.Create(context.TODO(), s) } func Test_gatewayOperator_CreateGateway(t *testing.T) { @@ -508,6 +551,7 @@ func Test_gatewayOperator_UpgradeGateway(t *testing.T) { appsv1.AddToScheme(Scheme) client2 := fake.NewFakeClientWithScheme(Scheme) create_LegacyGateway(client2, "project2") + create_LegacyGatewayConfigMap(client2, "project2") tests := []struct { name string @@ -552,6 +596,9 @@ func Test_gatewayOperator_UpgradeGateway(t *testing.T) { Enabled: true, Namespace: "project2", }, + Config: map[string]string{ + "fake": "true", + }, }, Service: v1alpha1.ServiceSpec{ Annotations: map[string]string{ @@ -559,6 +606,12 @@ func Test_gatewayOperator_UpgradeGateway(t *testing.T) { }, Type: corev1.ServiceTypeNodePort, }, + Deployment: v1alpha1.DeploymentSpec{ + Replicas: &[]int32{1}[0], + Annotations: map[string]string{ + "sidecar.istio.io/inject": "true", + }, + }, }, }, }, @@ -580,3 +633,160 @@ func Test_gatewayOperator_UpgradeGateway(t *testing.T) { }) } } + +func Test_gatewayOperator_ListGateways(t *testing.T) { + type fields struct { + client client.Client + cache cache.Cache + options *gateway.Options + } + type args struct { + query *query.Query + } + + var Scheme = runtime.NewScheme() + v1alpha1.AddToScheme(Scheme) + corev1.AddToScheme(Scheme) + appsv1.AddToScheme(Scheme) + + client := fake.NewFakeClientWithScheme(Scheme) + + create_LegacyGateway(client, "project2") + + client.Create(context.TODO(), &v1alpha1.Gateway{ + ObjectMeta: v1.ObjectMeta{ + Name: "kubesphere-router-project1", + Namespace: "project1", + }, + }) + + gates := []*v1alpha1.Gateway{ + { + ObjectMeta: v1.ObjectMeta{ + Name: fmt.Sprint(gatewayPrefix, "project2"), + Namespace: "kubesphere-controls-system", + }, + Spec: v1alpha1.GatewaySpec{ + Conroller: v1alpha1.ControllerSpec{ + Scope: v1alpha1.Scope{ + Enabled: true, + Namespace: "project2", + }, + }, + Service: v1alpha1.ServiceSpec{ + Annotations: map[string]string{ + "fake": "true", + }, + Type: corev1.ServiceTypeNodePort, + }, + Deployment: v1alpha1.DeploymentSpec{ + Replicas: &[]int32{1}[0], + Annotations: map[string]string{ + SidecarInject: "true", + }, + }, + }, + }, + { + ObjectMeta: v1.ObjectMeta{ + Name: fmt.Sprint(gatewayPrefix, "project1"), + Namespace: "project1", + ResourceVersion: "1", + }, + }, + } + + items := make([]interface{}, 0) + for _, obj := range gates { + items = append(items, obj) + } + + tests := []struct { + name string + fields fields + args args + want *api.ListResult + wantErr bool + }{ + { + name: "list all gateways", + fields: fields{ + client: client, + cache: &fakeClient{Client: client}, + options: &gateway.Options{ + Namespace: "kubesphere-controls-system", + }, + }, + args: args{ + query: &query.Query{}, + }, + want: &api.ListResult{ + TotalItems: 2, + Items: items, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &gatewayOperator{ + client: tt.fields.client, + cache: tt.fields.cache, + options: tt.fields.options, + } + got, err := c.ListGateways(tt.args.query) + if (err != nil) != tt.wantErr { + t.Errorf("gatewayOperator.ListGateways() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("gatewayOperator.ListGateways() has wrong object\nDiff:\n %s", diff.ObjectGoPrintSideBySide(tt.want, got)) + } + }) + } +} + +type fakeClient struct { + Client client.Client +} + +// Get retrieves an obj for the given object key from the Kubernetes Cluster. +// obj must be a struct pointer so that obj can be updated with the response +// returned by the Server. +func (f *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object) error { + return f.Client.Get(ctx, key, obj) +} + +// List retrieves list of objects for a given namespace and list options. On a +// successful call, Items field in the list will be populated with the +// result returned from the server. +func (f *fakeClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + return f.Client.List(ctx, list, opts...) +} + +// GetInformer fetches or constructs an informer for the given object that corresponds to a single +// API kind and resource. +func (f *fakeClient) GetInformer(ctx context.Context, obj client.Object) (cache.Informer, error) { + return nil, nil +} + +// GetInformerForKind is similar to GetInformer, except that it takes a group-version-kind, instead +// of the underlying object. +func (f *fakeClient) GetInformerForKind(ctx context.Context, gvk schema.GroupVersionKind) (cache.Informer, error) { + return nil, nil +} + +// Start runs all the informers known to this cache until the context is closed. +// It blocks. +func (f *fakeClient) Start(ctx context.Context) error { + return nil +} + +// WaitForCacheSync waits for all the caches to sync. Returns false if it could not sync a cache. +func (f *fakeClient) WaitForCacheSync(ctx context.Context) bool { + return false +} + +func (f *fakeClient) IndexField(ctx context.Context, obj client.Object, field string, extractValue client.IndexerFunc) error { + return nil +}