This is an automated email from the ASF dual-hosted git repository.

hanahmily pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking-swck.git


The following commit(s) were added to refs/heads/master by this push:
     new e13b753  Add unit test case for operator (#16)
e13b753 is described below

commit e13b753a88adaaa70a8cc2af47f42ac4c04a747a
Author: Gao Hongtao <[email protected]>
AuthorDate: Fri Dec 18 11:00:32 2020 +0800

    Add unit test case for operator (#16)
    
    * Add test case for operator
    
    Signed-off-by: Gao Hongtao <[email protected]>
    
    * Fix lint issues
    
    Signed-off-by: Gao Hongtao <[email protected]>
    
    * Install kubebuilder for ci
    
    Signed-off-by: Gao Hongtao <[email protected]>
    
    * change shell file mode
    
    Signed-off-by: Gao Hongtao <[email protected]>
    
    * Update check name
    
    Signed-off-by: Gao Hongtao <[email protected]>
    
    * Restore check name
    
    Signed-off-by: Gao Hongtao <[email protected]>
---
 .asf.yaml                                         |   2 +-
 .github/workflows/go.yml                          |  47 ++++++--
 CHANGES.md                                        |   2 +
 controllers/operator/oapserver_controller.go      |  55 +++++----
 controllers/operator/oapserver_controller_test.go | 130 ++++++++++++++++++++++
 controllers/operator/suite_test.go                |  69 ++++++++++++
 go.mod                                            |   1 +
 .asf.yaml => hack/install-kubebuilder.sh          |  32 ++----
 pkg/kubernetes/kubernetes.go                      |  16 +++
 9 files changed, 297 insertions(+), 57 deletions(-)

diff --git a/.asf.yaml b/.asf.yaml
index 5dadefc..53f174d 100644
--- a/.asf.yaml
+++ b/.asf.yaml
@@ -34,7 +34,7 @@ github:
       required_status_checks:
         strict: true
         contexts:
-          - build
+          - build 
       required_pull_request_reviews:
         dismiss_stale_reviews: true
         required_approving_review_count: 1
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index eb558f4..2f1c139 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -14,7 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-name: Build
+name: Continuous Integration
 
 on:
   pull_request:
@@ -23,11 +23,29 @@ on:
       - master
 
 jobs:
+  check:
+    name: Check
+    runs-on: ubuntu-20.04
+    steps:
+      - name: Install Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: 1.14
+          id: go
+      - name: Check out code into the Go module directory
+        uses: actions/checkout@v2
+      - name: Update dependencies
+        run: GOPROXY=https://proxy.golang.org go mod download
+      - name: Lint
+        run: make lint
+      - name: Check
+        run: make check
   build:
+    name: Build
     strategy:
       matrix:
         go-version: [ 1.14.x, 1.15.x ]
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     steps:
       - name: Install Go
         uses: actions/setup-go@v2
@@ -37,17 +55,30 @@ jobs:
         uses: actions/checkout@v2
       - name: Update dependencies 
         run: GOPROXY=https://proxy.golang.org go mod download
-      - name: Lint
-        run: make lint
-      - name: Check
-        run: make check
       - name: Build
         run: make
       - name: Build docker image
         run: make docker-build
+  unit-tests:
+    name: Unit tests
+    runs-on: ubuntu-20.04
+    steps:
+      - name: Install Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: 1.14
+        id: go
+      - name: Check out code into the Go module directory
+        uses: actions/checkout@v2
+      - name: Update dependencies
+        run: GOPROXY=https://proxy.golang.org go mod download
+      - name: "install kubebuilder"
+        run: ./hack/install-kubebuilder.sh
+      - name: tests
+        run: make test
   checks:
     name: build
-    runs-on: ubuntu-latest
-    needs: [build]
+    runs-on: ubuntu-20.04
+    needs: [check, build, unit-tests]
     steps:
       - run: echo 'success'
diff --git a/CHANGES.md b/CHANGES.md
index 54cd700..8787733 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -7,9 +7,11 @@ Release Notes.
 
 #### Features
 - Introduce custom metrics adapter to SkyWalking OAP cluster for Kubernetes 
HPA autoscaling.
+- Add RBAC files and service account to support Kubernetes coordination.
 
 #### Chores
 - Transform project layers to support multiple applications.
+- Introduce unit test to verify the operator.
 
 0.1.0
 ------------------
diff --git a/controllers/operator/oapserver_controller.go 
b/controllers/operator/oapserver_controller.go
index 76d2d26..0d55c04 100644
--- a/controllers/operator/oapserver_controller.go
+++ b/controllers/operator/oapserver_controller.go
@@ -24,9 +24,11 @@ import (
        "time"
 
        "github.com/go-logr/logr"
+       l "github.com/sirupsen/logrus"
        apps "k8s.io/api/apps/v1"
        core "k8s.io/api/core/v1"
        apiequal "k8s.io/apimachinery/pkg/api/equality"
+       apierrors "k8s.io/apimachinery/pkg/api/errors"
        "k8s.io/apimachinery/pkg/runtime"
        ctrl "sigs.k8s.io/controller-runtime"
        "sigs.k8s.io/controller-runtime/pkg/client"
@@ -38,7 +40,6 @@ import (
 const annotationKeyIstioSetup = "istio-setup-command"
 
 var schedDuration, _ = time.ParseDuration("1m")
-var rushModeSchedDuration, _ = time.ParseDuration("5s")
 
 // OAPServerReconciler reconciles a OAPServer object
 type OAPServerReconciler struct {
@@ -82,9 +83,17 @@ func (r *OAPServerReconciler) Reconcile(ctx context.Context, 
req ctrl.Request) (
                }
        }
 
-       r.istio(ctx, log, oapServer.Name, &oapServer)
+       if err := r.istio(ctx, log, oapServer.Name, &oapServer); err != nil {
+               l.Error(err, "failed to sync istio annotation")
+               return ctrl.Result{}, err
+       }
 
-       return ctrl.Result{RequeueAfter: r.checkState(ctx, log, &oapServer)}, 
nil
+       if err := r.checkState(ctx, log, &oapServer); err != nil {
+               l.Error(err, "failed to check sub resources state")
+               return ctrl.Result{}, err
+       }
+
+       return ctrl.Result{RequeueAfter: schedDuration}, nil
 }
 
 func tmplFunc(oapServer *operatorv1alpha1.OAPServer) template.FuncMap {
@@ -110,50 +119,48 @@ func tmplFunc(oapServer *operatorv1alpha1.OAPServer) 
template.FuncMap {
        }
 }
 
-func (r *OAPServerReconciler) checkState(ctx context.Context, log logr.Logger, 
oapServer *operatorv1alpha1.OAPServer) time.Duration {
+func (r *OAPServerReconciler) checkState(ctx context.Context, log logr.Logger, 
oapServer *operatorv1alpha1.OAPServer) error {
        overlay := operatorv1alpha1.OAPServerStatus{}
        deployment := apps.Deployment{}
-       nextSchedule := schedDuration
-       if err := r.Client.Get(ctx, client.ObjectKey{Namespace: 
oapServer.Namespace, Name: oapServer.Name}, &deployment); err != nil {
-               nextSchedule = rushModeSchedDuration
+       errCol := new(kubernetes.ErrorCollector)
+       if err := r.Client.Get(ctx, client.ObjectKey{Namespace: 
oapServer.Namespace, Name: oapServer.Name}, &deployment); err != nil && 
!apierrors.IsNotFound(err) {
+               errCol.Collect(fmt.Errorf("failed to get deployment: %w", err))
        } else {
                overlay.Conditions = deployment.Status.Conditions
                overlay.AvailableReplicas = deployment.Status.AvailableReplicas
-               if oapServer.Spec.Instances != overlay.AvailableReplicas {
-                       nextSchedule = rushModeSchedDuration
-               }
                if oapServer.Spec.Image != 
deployment.Spec.Template.Spec.Containers[0].Image {
                        oapServer.Spec.Image = 
deployment.Spec.Template.Spec.Containers[0].Image
                        if err := r.Update(ctx, oapServer); err != nil {
-                               log.Error(err, "failed to update OAPServer 
Image field")
+                               errCol.Collect(fmt.Errorf("failed to update 
image field: %w", err))
+                               return errCol.Error()
                        }
                        log.Info("updated OAPServer Image")
-                       nextSchedule = rushModeSchedDuration
                }
        }
        service := core.Service{}
-       if err := r.Client.Get(ctx, client.ObjectKey{Namespace: 
oapServer.Namespace, Name: oapServer.Name}, &service); err != nil {
-               nextSchedule = rushModeSchedDuration
+       if err := r.Client.Get(ctx, client.ObjectKey{Namespace: 
oapServer.Namespace, Name: oapServer.Name}, &service); err != nil && 
!apierrors.IsNotFound(err) {
+               errCol.Collect(fmt.Errorf("failed to get service: %w", err))
        } else {
                overlay.Address = fmt.Sprintf("%s.%s", service.Name, 
service.Namespace)
        }
        if apiequal.Semantic.DeepDerivative(overlay, oapServer.Status) {
                log.Info("Status keeps the same as before")
-               return nextSchedule
        }
        oapServer.Status = overlay
+       oapServer.Kind = "OAPServer"
        if err := kubernetes.ApplyOverlay(oapServer, 
&operatorv1alpha1.OAPServer{Status: overlay}); err != nil {
-               log.Error(err, "failed to overlay OAPServer")
-               return rushModeSchedDuration
+               errCol.Collect(fmt.Errorf("failed to apply overlay: %w", err))
+               return errCol.Error()
        }
        if err := r.Status().Update(ctx, oapServer); err != nil {
-               return rushModeSchedDuration
+               errCol.Collect(fmt.Errorf("failed to update status of 
OAPServer: %w", err))
        }
        log.Info("updated Status sub resource")
-       return nextSchedule
+
+       return errCol.Error()
 }
 
-func (r *OAPServerReconciler) istio(ctx context.Context, log logr.Logger, 
serviceName string, oapServer *operatorv1alpha1.OAPServer) {
+func (r *OAPServerReconciler) istio(ctx context.Context, log logr.Logger, 
serviceName string, oapServer *operatorv1alpha1.OAPServer) error {
        for _, envVar := range oapServer.Spec.Config {
                if envVar.Name == "SW_ENVOY_METRIC_ALS_HTTP_ANALYSIS" &&
                        
oapServer.ObjectMeta.Annotations[annotationKeyIstioSetup] == "" {
@@ -161,17 +168,19 @@ func (r *OAPServerReconciler) istio(ctx context.Context, 
log logr.Logger, servic
                                "--set 
meshConfig.defaultConfig.envoyAccessLogService.address=%s.%s:11800 "+
                                "--set 
meshConfig.enableEnvoyAccessLogService=true", serviceName, oapServer.Namespace)
                        if err := r.Update(ctx, oapServer); err != nil {
-                               log.Error(err, "unable to patch Istio setup 
command to annotation")
-                               return
+                               return err
                        }
                        log.Info("patched Istio annotation")
-                       return
+                       return nil
                }
        }
+       return nil
 }
 
 func (r *OAPServerReconciler) SetupWithManager(mgr ctrl.Manager) error {
        return ctrl.NewControllerManagedBy(mgr).
                For(&operatorv1alpha1.OAPServer{}).
+               Owns(&apps.Deployment{}).
+               Owns(&core.Service{}).
                Complete(r)
 }
diff --git a/controllers/operator/oapserver_controller_test.go 
b/controllers/operator/oapserver_controller_test.go
new file mode 100644
index 0000000..96c459a
--- /dev/null
+++ b/controllers/operator/oapserver_controller_test.go
@@ -0,0 +1,130 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you 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 controllers_test
+
+import (
+       "context"
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+       appsv1 "k8s.io/api/apps/v1"
+       corev1 "k8s.io/api/core/v1"
+       rbacv1 "k8s.io/api/rbac/v1"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/apimachinery/pkg/types"
+       "sigs.k8s.io/controller-runtime/pkg/client"
+       logf "sigs.k8s.io/controller-runtime/pkg/log"
+       k8sreconcile "sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+       "github.com/apache/skywalking-swck/apis/operator/v1alpha1"
+       controllers "github.com/apache/skywalking-swck/controllers/operator"
+       "github.com/apache/skywalking-swck/pkg/operator/repo"
+)
+
+var logger = logf.Log.WithName("unit-tests")
+var fileRepo = repo.NewRepo("oapserver")
+
+func TestNewObjectsOnReconciliation(t *testing.T) {
+       // prepare
+       nsn := types.NamespacedName{Name: "my-instance", Namespace: "default"}
+       reconciler := controllers.OAPServerReconciler{
+               Client:   k8sClient,
+               Log:      logger,
+               Scheme:   testScheme,
+               FileRepo: fileRepo,
+       }
+       created := &v1alpha1.OAPServer{
+               TypeMeta: metav1.TypeMeta{
+                       Kind:       "OAPServer",
+                       APIVersion: v1alpha1.GroupVersion.Version,
+               },
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      nsn.Name,
+                       Namespace: nsn.Namespace,
+               },
+               Spec: v1alpha1.OAPServerSpec{
+                       Instances: 1,
+               },
+       }
+       err := k8sClient.Create(context.Background(), created)
+       require.NoError(t, err)
+
+       // test
+       req := k8sreconcile.Request{
+               NamespacedName: nsn,
+       }
+       _, err = reconciler.Reconcile(context.Background(), req)
+
+       // verify
+       require.NoError(t, err)
+
+       // the base query for the underlying objects
+       opts := []client.ListOption{
+               client.InNamespace(nsn.Namespace),
+               client.MatchingLabels(map[string]string{
+                       "operator.skywalking.apache.org/oap-server-name": 
nsn.Name,
+                       "operator.skywalking.apache.org/application":     
"oapserver",
+               }),
+       }
+
+       // verify that we have at least one object for each of the types we 
create
+       // whether we have the right ones is up to the specific tests for each 
type
+       {
+               list := &corev1.ServiceAccountList{}
+               err = k8sClient.List(context.Background(), list, opts...)
+               assert.NoError(t, err)
+               assert.NotEmpty(t, list.Items)
+       }
+       {
+               list := &corev1.ServiceList{}
+               err = k8sClient.List(context.Background(), list, opts...)
+               assert.NoError(t, err)
+               assert.NotEmpty(t, list.Items)
+       }
+       {
+               list := &appsv1.DeploymentList{}
+               err = k8sClient.List(context.Background(), list, opts...)
+               assert.NoError(t, err)
+               assert.NotEmpty(t, list.Items)
+       }
+
+       // the base query for the underlying objects
+       rbacOpts := []client.ListOption{
+               client.MatchingLabels(map[string]string{
+                       "operator.skywalking.apache.org/application": 
"oapserver",
+                       "operator.skywalking.apache.org/component":   "rbac",
+               }),
+       }
+       {
+               list := &rbacv1.ClusterRoleBindingList{}
+               err = k8sClient.List(context.Background(), list, rbacOpts...)
+               assert.NoError(t, err)
+               assert.NotEmpty(t, list.Items)
+       }
+       {
+               list := &rbacv1.ClusterRoleList{}
+               err = k8sClient.List(context.Background(), list, rbacOpts...)
+               assert.NoError(t, err)
+               assert.NotEmpty(t, list.Items)
+       }
+
+       // cleanup
+       require.NoError(t, k8sClient.Delete(context.Background(), created))
+
+}
diff --git a/controllers/operator/suite_test.go 
b/controllers/operator/suite_test.go
new file mode 100644
index 0000000..7b6c5e7
--- /dev/null
+++ b/controllers/operator/suite_test.go
@@ -0,0 +1,69 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you 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 controllers_test
+
+import (
+       "fmt"
+       "os"
+       "path/filepath"
+       "testing"
+
+       "k8s.io/apimachinery/pkg/runtime"
+       "k8s.io/client-go/kubernetes/scheme"
+       "sigs.k8s.io/controller-runtime/pkg/client"
+       "sigs.k8s.io/controller-runtime/pkg/envtest"
+
+       "github.com/apache/skywalking-swck/apis/operator/v1alpha1"
+)
+
+var k8sClient client.Client
+var testEnv *envtest.Environment
+var testScheme *runtime.Scheme = scheme.Scheme
+
+func TestMain(m *testing.M) {
+       testEnv = &envtest.Environment{
+               CRDDirectoryPaths: []string{filepath.Join("../..", "config", 
"operator", "crd", "bases")},
+       }
+
+       cfg, err := testEnv.Start()
+       if err != nil {
+               fmt.Printf("failed to start testEnv: %v", err)
+               os.Exit(1)
+       }
+
+       if errAddScheme := v1alpha1.AddToScheme(testScheme); errAddScheme != 
nil {
+               fmt.Printf("failed to register scheme: %v", errAddScheme)
+               os.Exit(1)
+       }
+
+       k8sClient, err = client.New(cfg, client.Options{Scheme: testScheme})
+       if err != nil {
+               fmt.Printf("failed to setup a Kubernetes client: %v", err)
+               os.Exit(1)
+       }
+
+       code := m.Run()
+
+       err = testEnv.Stop()
+       if err != nil {
+               fmt.Printf("failed to stop testEnv: %v", err)
+               os.Exit(1)
+       }
+
+       os.Exit(code)
+}
diff --git a/go.mod b/go.mod
index 9877ff3..0d7d8ab 100644
--- a/go.mod
+++ b/go.mod
@@ -12,6 +12,7 @@ require (
        github.com/machinebox/graphql v0.2.2
        github.com/sirupsen/logrus v1.7.0
        github.com/spf13/cobra v1.0.0
+       github.com/stretchr/testify v1.6.1
        github.com/urfave/cli v1.22.1
        k8s.io/api v0.19.3
        k8s.io/apiextensions-apiserver v0.19.3 // indirect
diff --git a/.asf.yaml b/hack/install-kubebuilder.sh
old mode 100644
new mode 100755
similarity index 59%
copy from .asf.yaml
copy to hack/install-kubebuilder.sh
index 5dadefc..c5fdcc5
--- a/.asf.yaml
+++ b/hack/install-kubebuilder.sh
@@ -1,3 +1,5 @@
+#!/usr/bin/env bash
+
 #
 # Licensed to the Apache Software Foundation (ASF) under one or more
 # contributor license agreements.  See the NOTICE file distributed with
@@ -13,29 +15,9 @@
 # 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.
-#
-
-github:
-  description: Apache SkyWalking Cloud on Kubernetes
-  homepage: https://skywalking.apache.org/
-  labels:
-    - skywalking
-    - observability
-    - apm
-    - distributed-tracing
-    - kubernetes
-    - operator
-  enabled_merge_buttons:
-    squash:  true
-    merge:   false
-    rebase:  false
-  protected_branches:
-    master:
-      required_status_checks:
-        strict: true
-        contexts:
-          - build
-      required_pull_request_reviews:
-        dismiss_stale_reviews: true
-        required_approving_review_count: 1
 
+os=$(go env GOOS)
+arch=$(go env GOARCH)
+curl -L https://go.kubebuilder.io/dl/2.3.1/${os}/${arch} | tar -xz -C /tmp/
+sudo mv /tmp/kubebuilder_2.3.1_${os}_${arch} /usr/local/kubebuilder
+export PATH=$PATH:/usr/local/kubebuilder/bin
diff --git a/pkg/kubernetes/kubernetes.go b/pkg/kubernetes/kubernetes.go
index 91a65cf..fd56e65 100644
--- a/pkg/kubernetes/kubernetes.go
+++ b/pkg/kubernetes/kubernetes.go
@@ -19,6 +19,7 @@ package kubernetes
 
 import (
        "bytes"
+       "fmt"
        "text/template"
 
        "github.com/Masterminds/sprig/v3"
@@ -63,3 +64,18 @@ func LoadTemplate(manifest string, values interface{}, 
funcMap template.FuncMap,
        }
        return yaml.Unmarshal(buf.Bytes(), spec)
 }
+
+type ErrorCollector []error
+
+func (c *ErrorCollector) Collect(e error) { *c = append(*c, e) }
+
+func (c *ErrorCollector) Error() error {
+       if len(*c) < 1 {
+               return nil
+       }
+       err := "Collected errors:\n"
+       for i, e := range *c {
+               err += fmt.Sprintf("\tError %d: %s\n", i, e.Error())
+       }
+       return fmt.Errorf(err)
+}

Reply via email to