feat: kubesphere 4.0 (#6115)

* feat: kubesphere 4.0

Signed-off-by: ci-bot <ci-bot@kubesphere.io>

* feat: kubesphere 4.0

Signed-off-by: ci-bot <ci-bot@kubesphere.io>

---------

Signed-off-by: ci-bot <ci-bot@kubesphere.io>
Co-authored-by: ks-ci-bot <ks-ci-bot@example.com>
Co-authored-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
KubeSphere CI Bot
2024-09-06 11:05:52 +08:00
committed by GitHub
parent b5015ec7b9
commit 447a51f08b
8557 changed files with 546695 additions and 1146174 deletions

View File

@@ -1,18 +1,7 @@
/*
Copyright 2019 The KubeSphere 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.
*/
* Please refer to the LICENSE file in the root directory of the project.
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
*/
package job
@@ -21,173 +10,74 @@ import (
"encoding/json"
"fmt"
"reflect"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"time"
batchv1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
batchv1informers "k8s.io/client-go/informers/batch/v1"
batchv1listers "k8s.io/client-go/listers/batch/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/klog/v2"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
kscontroller "kubesphere.io/kubesphere/pkg/controller"
)
const (
// maxRetries is the number of times a service will be retried before it is dropped out of the queue.
// With the current rate-limiter in use (5ms*2^(maxRetries-1)) the following numbers represent the
// sequence of delays between successive queuings of a service.
//
// 5ms, 10ms, 20ms, 40ms, 80ms, 160ms, 320ms, 640ms, 1.3s, 2.6s, 5.1s, 10.2s, 20.4s, 41s, 82s
maxRetries = 15
revisionsAnnotationKey = "revisions"
controllerName = "job-revision"
)
type JobController struct {
client clientset.Interface
var _ kscontroller.Controller = &Reconciler{}
var _ reconcile.Reconciler = &Reconciler{}
jobLister batchv1listers.JobLister
jobSynced cache.InformerSynced
queue workqueue.RateLimitingInterface
workerLoopPeriod time.Duration
type Reconciler struct {
client.Client
}
func NewJobController(jobInformer batchv1informers.JobInformer, client clientset.Interface) *JobController {
v := &JobController{
client: client,
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "job"),
workerLoopPeriod: time.Second,
}
v.jobLister = jobInformer.Lister()
v.jobSynced = jobInformer.Informer().HasSynced
jobInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: v.enqueueJob,
UpdateFunc: func(old, cur interface{}) {
v.enqueueJob(cur)
},
})
return v
func (r *Reconciler) Name() string {
return controllerName
}
func (v *JobController) Start(ctx context.Context) error {
return v.Run(5, ctx.Done())
func (r *Reconciler) SetupWithManager(mgr *kscontroller.Manager) error {
r.Client = mgr.GetClient()
return builder.
ControllerManagedBy(mgr).
For(
&batchv1.Job{},
builder.WithPredicates(
predicate.ResourceVersionChangedPredicate{},
),
).
WithOptions(controller.Options{
MaxConcurrentReconciles: 2,
}).
Complete(r)
}
func (v *JobController) Run(workers int, stopCh <-chan struct{}) error {
defer utilruntime.HandleCrash()
defer v.queue.ShutDown()
klog.Info("starting job controller")
defer klog.Info("shutting down job controller")
if !cache.WaitForCacheSync(stopCh, v.jobSynced) {
return fmt.Errorf("failed to wait for caches to sync")
}
for i := 0; i < workers; i++ {
go wait.Until(v.worker, v.workerLoopPeriod, stopCh)
}
<-stopCh
return nil
}
func (v *JobController) enqueueJob(obj interface{}) {
key, err := cache.MetaNamespaceKeyFunc(obj)
if err != nil {
utilruntime.HandleError(fmt.Errorf("couldn't get key for object %+v: %v", obj, err))
return
}
v.queue.Add(key)
}
func (v *JobController) worker() {
for v.processNextWorkItem() {
}
}
func (v *JobController) processNextWorkItem() bool {
eKey, quit := v.queue.Get()
if quit {
return false
}
defer v.queue.Done(eKey)
err := v.syncJob(eKey.(string))
v.handleErr(err, eKey)
return true
}
// main function of the reconcile for job
// job's name is same with the service that created it
func (v *JobController) syncJob(key string) error {
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
startTime := time.Now()
defer func() {
klog.V(4).Info("Finished syncing job.", "key", key, "duration", time.Since(startTime))
klog.V(4).Info("Finished syncing job.", "key", req.String(), "duration", time.Since(startTime))
}()
namespace, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
return err
job := &batchv1.Job{}
if err := r.Get(ctx, req.NamespacedName, job); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
job, err := v.jobLister.Jobs(namespace).Get(name)
if err != nil {
// has been deleted
if errors.IsNotFound(err) {
return nil
}
klog.Error(err, "get job failed", "namespace", namespace, "name", name)
return err
if err := r.makeRevision(ctx, job); err != nil {
klog.Error(err, "make job revision failed", "namespace", req.Namespace, "name", req.Name)
return ctrl.Result{}, err
}
err = v.makeRevision(job)
if err != nil {
klog.Error(err, "make job revision failed", "namespace", namespace, "name", name)
return err
}
return nil
return ctrl.Result{}, nil
}
func (v *JobController) handleErr(err error, key interface{}) {
if err == nil {
v.queue.Forget(key)
return
}
if v.queue.NumRequeues(key) < maxRetries {
klog.V(2).Info("Error syncing job, retrying.", "key", key, "error", err)
v.queue.AddRateLimited(key)
return
}
klog.V(4).Info("Dropping job out of the queue", "key", key, "error", err)
v.queue.Forget(key)
utilruntime.HandleError(err)
}
func (v *JobController) makeRevision(job *batchv1.Job) error {
func (r *Reconciler) makeRevision(ctx context.Context, job *batchv1.Job) error {
revisionIndex := -1
revisions, err := v.getRevisions(job)
revisions, err := r.getRevisions(job)
// failed get revisions
if err != nil {
return nil
@@ -196,7 +86,7 @@ func (v *JobController) makeRevision(job *batchv1.Job) error {
uid := job.UID
for index, revision := range revisions {
if revision.Uid == string(uid) {
currentRevision := v.getCurrentRevision(job)
currentRevision := r.getCurrentRevision(job)
if reflect.DeepEqual(currentRevision, revision) {
return nil
} else {
@@ -210,7 +100,7 @@ func (v *JobController) makeRevision(job *batchv1.Job) error {
revisionIndex = len(revisions) + 1
}
revisions[revisionIndex] = v.getCurrentRevision(job)
revisions[revisionIndex] = r.getCurrentRevision(job)
revisionsByte, err := json.Marshal(revisions)
if err != nil {
@@ -221,17 +111,11 @@ func (v *JobController) makeRevision(job *batchv1.Job) error {
if job.Annotations == nil {
job.Annotations = make(map[string]string)
}
job.Annotations[revisionsAnnotationKey] = string(revisionsByte)
_, err = v.client.BatchV1().Jobs(job.Namespace).Update(context.Background(), job, metav1.UpdateOptions{})
if err != nil {
return err
}
return nil
return r.Update(ctx, job)
}
func (v *JobController) getRevisions(job *batchv1.Job) (JobRevisions, error) {
func (r *Reconciler) getRevisions(job *batchv1.Job) (JobRevisions, error) {
revisions := make(JobRevisions)
if revisionsStr := job.Annotations[revisionsAnnotationKey]; revisionsStr != "" {
@@ -244,14 +128,14 @@ func (v *JobController) getRevisions(job *batchv1.Job) (JobRevisions, error) {
return revisions, nil
}
func (v *JobController) getCurrentRevision(item *batchv1.Job) JobRevision {
func (r *Reconciler) getCurrentRevision(item *batchv1.Job) JobRevision {
var revision JobRevision
for _, condition := range item.Status.Conditions {
if condition.Type == batchv1.JobFailed && condition.Status == v1.ConditionTrue {
if condition.Type == batchv1.JobFailed && condition.Status == corev1.ConditionTrue {
revision.Status = Failed
revision.Reasons = append(revision.Reasons, condition.Reason)
revision.Messages = append(revision.Messages, condition.Message)
} else if condition.Type == batchv1.JobComplete && condition.Status == v1.ConditionTrue {
} else if condition.Type == batchv1.JobComplete && condition.Status == corev1.ConditionTrue {
revision.Status = Completed
}
}

View File

@@ -1,72 +1,25 @@
/*
Copyright 2019 The KubeSphere 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.
*/
* Please refer to the LICENSE file in the root directory of the project.
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
*/
package job
import (
"reflect"
"context"
"testing"
"time"
batchv1 "k8s.io/api/batch/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/diff"
kubeinformers "k8s.io/client-go/informers"
k8sfake "k8s.io/client-go/kubernetes/fake"
core "k8s.io/client-go/testing"
"k8s.io/client-go/tools/cache"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"kubesphere.io/kubesphere/pkg/scheme"
)
var (
noResyncPeriodFunc = func() time.Duration { return 0 }
)
type fixture struct {
t *testing.T
kubeclient *k8sfake.Clientset
//nolint:unused
jobController *JobController
jobLister []*batchv1.Job
//nolint:unused
kubeactions []core.Action
actions []core.Action
kubeobjects []runtime.Object
objects []runtime.Object
}
func filterInformerActions(actions []core.Action) []core.Action {
ret := []core.Action{}
for _, action := range actions {
if len(action.GetNamespace()) == 0 &&
(action.Matches("list", "jobs") ||
action.Matches("watch", "jobs")) {
continue
}
ret = append(ret, action)
}
return ret
}
func newJob(name string, spec batchv1.JobSpec) *batchv1.Job {
job := &batchv1.Job{
return &batchv1.Job{
TypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String()},
ObjectMeta: metav1.ObjectMeta{
Name: name,
@@ -74,136 +27,35 @@ func newJob(name string, spec batchv1.JobSpec) *batchv1.Job {
},
Spec: spec,
}
return job
}
func newFixture(t *testing.T) *fixture {
f := &fixture{}
f.t = t
f.objects = []runtime.Object{}
f.kubeobjects = []runtime.Object{}
return f
}
func checkAction(expected, actual core.Action, t *testing.T) {
if !(expected.Matches(actual.GetVerb(), actual.GetResource().Resource) && actual.GetSubresource() == expected.GetSubresource()) {
t.Errorf("Expected\n\t%#v\ngot\n\t%#v", expected, actual)
return
}
if reflect.TypeOf(actual) != reflect.TypeOf(expected) {
t.Errorf("Action has wrong type. Expected: %t. Got: %t", expected, actual)
return
}
switch a := actual.(type) {
case core.CreateActionImpl:
e, _ := expected.(core.CreateActionImpl)
expObject := e.GetObject()
object := a.GetObject()
if !reflect.DeepEqual(expObject, object) {
t.Errorf("Action %s %s has wrong object\nDiff:\n %s",
a.GetVerb(), a.GetResource().Resource, diff.ObjectGoPrintSideBySide(expObject, object))
}
case core.UpdateActionImpl:
e, _ := expected.(core.UpdateActionImpl)
expObject := e.GetObject()
object := a.GetObject()
if !reflect.DeepEqual(expObject, object) {
t.Errorf("Action %s %s has wrong object\nDiff:\n %s",
a.GetVerb(), a.GetResource().Resource, diff.ObjectGoPrintSideBySide(expObject, object))
}
case core.PatchActionImpl:
e, _ := expected.(core.PatchActionImpl)
expPatch := e.GetPatch()
patch := a.GetPatch()
if !reflect.DeepEqual(expPatch, patch) {
t.Errorf("Action %s %s has wrong patch\nDiff:\n %s",
a.GetVerb(), a.GetResource().Resource, diff.ObjectGoPrintSideBySide(expPatch, patch))
}
default:
t.Errorf("Uncaptured Action %s %s, you should explicitly add a case to capture it",
actual.GetVerb(), actual.GetResource().Resource)
}
}
func (f *fixture) newController() (*JobController, kubeinformers.SharedInformerFactory) {
f.kubeclient = k8sfake.NewSimpleClientset(f.kubeobjects...)
k8sI := kubeinformers.NewSharedInformerFactory(f.kubeclient, noResyncPeriodFunc())
jobController := NewJobController(k8sI.Batch().V1().Jobs(), f.kubeclient)
for _, job := range f.jobLister {
_ = k8sI.Batch().V1().Jobs().Informer().GetIndexer().Add(job)
}
return jobController, k8sI
}
func (f *fixture) runController(jobName string, startInformers bool, expectError bool) {
c, k8sI := f.newController()
if startInformers {
stopCh := make(chan struct{})
defer close(stopCh)
k8sI.Start(stopCh)
}
err := c.syncJob(jobName)
if !expectError && err != nil {
f.t.Errorf("error syncing job: %v", err)
} else if expectError && err == nil {
f.t.Error("expected error syncing job, got nil")
}
actions := filterInformerActions(f.kubeclient.Actions())
for i, action := range actions {
if len(f.actions) < i+1 {
f.t.Errorf("%d unexpected actions: %+v", len(actions)-len(f.actions), actions[i:])
break
}
expectedAction := f.actions[i]
checkAction(expectedAction, action, f.t)
}
if len(f.actions) > len(actions) {
f.t.Errorf("%d additional expected actions:%+v", len(f.actions)-len(actions), f.actions[len(actions):])
}
}
func (f *fixture) expectAddAnnotationAction(job *batchv1.Job) {
action := core.NewUpdateAction(schema.GroupVersionResource{Resource: "jobs"}, job.Namespace, job)
f.actions = append(f.actions, action)
}
func (f *fixture) run(jobName string) {
f.runController(jobName, true, false)
}
func TestAddAnnotation(t *testing.T) {
f := newFixture(t)
job := newJob("test", batchv1.JobSpec{})
f.jobLister = append(f.jobLister, job)
f.objects = append(f.objects, job)
fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(job).Build()
f.kubeobjects = append(f.kubeobjects, job)
reconciler := &Reconciler{}
reconciler.Client = fakeClient
f.expectAddAnnotationAction(job)
f.run(getKey(job, t))
}
func getKey(job *batchv1.Job, t *testing.T) string {
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(job)
if err != nil {
t.Errorf("Unexpected error getting key for job %v: %v", job.Name, err)
return ""
tests := []struct {
name string
req types.NamespacedName
isErr bool
}{
{
name: "normal test",
req: types.NamespacedName{
Namespace: job.Namespace,
Name: job.Name,
},
isErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if _, err := reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: tt.req}); tt.isErr != (err != nil) {
t.Errorf("%s Reconcile() unexpected error: %v", tt.name, err)
}
})
}
return key
}

View File

@@ -1,18 +1,7 @@
/*
Copyright 2019 The KubeSphere 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.
*/
* Please refer to the LICENSE file in the root directory of the project.
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
*/
package job