From 6fc5baaca06cf18532a88e21d02b99b141716199 Mon Sep 17 00:00:00 2001 From: Rick <1450685+LinuxSuRen@users.noreply.github.com> Date: Fri, 19 Feb 2021 21:24:31 +0800 Subject: [PATCH 01/21] Fix the issues that devops credentials cannot be deleted Signed-off-by: rick <1450685+LinuxSuRen@users.noreply.github.com> --- pkg/apis/devops/v1alpha3/credential_types.go | 5 +- .../devopscredential_controller.go | 55 +++++++++++++++---- .../pipeline/pipeline_controller.go | 31 +---------- pkg/controller/utils/hash.go | 34 ++++++++++++ 4 files changed, 83 insertions(+), 42 deletions(-) create mode 100644 pkg/controller/utils/hash.go diff --git a/pkg/apis/devops/v1alpha3/credential_types.go b/pkg/apis/devops/v1alpha3/credential_types.go index e712efd57..67ed703ed 100644 --- a/pkg/apis/devops/v1alpha3/credential_types.go +++ b/pkg/apis/devops/v1alpha3/credential_types.go @@ -23,8 +23,9 @@ We use a special type of secret as a credential for DevOps. This file will not contain CRD, but the credential type constants and their fields. */ const ( - CredentialFinalizerName = "finalizers.kubesphere.io/credential" - DevOpsCredentialPrefix = "credential.devops.kubesphere.io/" + CredentialFinalizerName = "finalizers.kubesphere.io/credential" + DevOpsCredentialPrefix = "credential.devops.kubesphere.io/" + DevOpsCredentialDataHash = DevOpsCredentialPrefix + "datahash" // SecretTypeBasicAuth contains data needed for basic authentication. // // Required at least one of fields: diff --git a/pkg/controller/devopscredential/devopscredential_controller.go b/pkg/controller/devopscredential/devopscredential_controller.go index a6e3f3298..ff9c3b9a8 100644 --- a/pkg/controller/devopscredential/devopscredential_controller.go +++ b/pkg/controller/devopscredential/devopscredential_controller.go @@ -19,6 +19,7 @@ package devopscredential import ( "context" "fmt" + "github.com/emicklei/go-restful" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -36,10 +37,12 @@ import ( devopsv1alpha3 "kubesphere.io/kubesphere/pkg/apis/devops/v1alpha3" kubesphereclient "kubesphere.io/kubesphere/pkg/client/clientset/versioned" "kubesphere.io/kubesphere/pkg/constants" + "kubesphere.io/kubesphere/pkg/controller/utils" modelsdevops "kubesphere.io/kubesphere/pkg/models/devops" devopsClient "kubesphere.io/kubesphere/pkg/simple/client/devops" "kubesphere.io/kubesphere/pkg/utils/k8sutil" "kubesphere.io/kubesphere/pkg/utils/sliceutil" + "net/http" "reflect" "strings" "time" @@ -218,7 +221,7 @@ func (c *Controller) syncHandler(key string) error { return err } if !isDevOpsProjectAdminNamespace(namespace) { - err := fmt.Errorf("cound not create credential in normal namespaces %s", namespace.Name) + err := fmt.Errorf("cound not create or update credential '%s' in normal namespaces %s", name, namespace.Name) klog.Warning(err) return err } @@ -233,14 +236,26 @@ func (c *Controller) syncHandler(key string) error { return err } - //If the sync is successful, return handle - if state, ok := secret.Annotations[devopsv1alpha3.CredentialSyncStatusAnnoKey]; ok && state == modelsdevops.StatusSuccessful { - return nil - } - copySecret := secret.DeepCopy() // DeletionTimestamp.IsZero() means copySecret has not been deleted. - if secret.ObjectMeta.DeletionTimestamp.IsZero() { + if copySecret.ObjectMeta.DeletionTimestamp.IsZero() { + // make sure Annotations is not nil + if copySecret.Annotations == nil { + copySecret.Annotations = map[string]string{} + } + + //If the sync is successful, return handle + if state, ok := copySecret.Annotations[devopsv1alpha3.CredentialSyncStatusAnnoKey]; ok && state == modelsdevops.StatusSuccessful { + specHash := utils.ComputeHash(copySecret.Data) + oldHash, _ := copySecret.Annotations[devopsv1alpha3.DevOpsCredentialDataHash] // don't need to check if it's nil, only compare if they're different + if specHash == oldHash { + // it was synced successfully, and there's any change with the Pipeline spec, skip this round + return nil + } else { + copySecret.Annotations[devopsv1alpha3.DevOpsCredentialDataHash] = specHash + } + } + // https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#finalizers if !sliceutil.HasString(secret.ObjectMeta.Finalizers, devopsv1alpha3.CredentialFinalizerName) { copySecret.ObjectMeta.Finalizers = append(copySecret.ObjectMeta.Finalizers, devopsv1alpha3.CredentialFinalizerName) @@ -268,13 +283,31 @@ func (c *Controller) syncHandler(key string) error { } else { // Finalizers processing logic if sliceutil.HasString(copySecret.ObjectMeta.Finalizers, devopsv1alpha3.CredentialFinalizerName) { + delSuccess := false if _, err := c.devopsClient.DeleteCredentialInProject(nsName, secret.Name); err != nil { + // the status code should be 404 if the credential does not exists + if srvErr, ok := err.(restful.ServiceError); ok { + delSuccess = srvErr.Code == http.StatusNotFound + } else if srvErr, ok := err.(*devopsClient.ErrorResponse); ok { + delSuccess = srvErr.Response.StatusCode == http.StatusNotFound + } else { + klog.Error(fmt.Sprintf("unexpected error type: %v, should be *restful.ServiceError", err)) + } + klog.V(8).Info(err, fmt.Sprintf("failed to delete secret %s in devops", key)) - return err + } else { + delSuccess = true + } + + if delSuccess { + copySecret.ObjectMeta.Finalizers = sliceutil.RemoveString(copySecret.ObjectMeta.Finalizers, func(item string) bool { + return item == devopsv1alpha3.CredentialFinalizerName + }) + } else { + // make sure the corresponding Jenkins credentials can be clean + // You can remove the finalizer via kubectl manually in a very special case that Jenkins might be not able to available anymore + return fmt.Errorf("failed to remove devops credential finalizer due to bad communication with Jenkins") } - copySecret.ObjectMeta.Finalizers = sliceutil.RemoveString(copySecret.ObjectMeta.Finalizers, func(item string) bool { - return item == devopsv1alpha3.CredentialFinalizerName - }) } } diff --git a/pkg/controller/pipeline/pipeline_controller.go b/pkg/controller/pipeline/pipeline_controller.go index 8b16fa90b..82ff027b9 100644 --- a/pkg/controller/pipeline/pipeline_controller.go +++ b/pkg/controller/pipeline/pipeline_controller.go @@ -19,14 +19,10 @@ package pipeline import ( "context" "fmt" - "github.com/davecgh/go-spew/spew" "github.com/emicklei/go-restful" - "hash" - "hash/fnv" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/rand" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" corev1informer "k8s.io/client-go/informers/core/v1" @@ -43,6 +39,7 @@ import ( devopsinformers "kubesphere.io/kubesphere/pkg/client/informers/externalversions/devops/v1alpha3" devopslisters "kubesphere.io/kubesphere/pkg/client/listers/devops/v1alpha3" "kubesphere.io/kubesphere/pkg/constants" + "kubesphere.io/kubesphere/pkg/controller/utils" modelsdevops "kubesphere.io/kubesphere/pkg/models/devops" devopsClient "kubesphere.io/kubesphere/pkg/simple/client/devops" "kubesphere.io/kubesphere/pkg/utils/k8sutil" @@ -240,7 +237,7 @@ func (c *Controller) syncHandler(key string) error { //If the sync is successful, return handle if state, ok := copyPipeline.Annotations[devopsv1alpha3.PipelineSyncStatusAnnoKey]; ok && state == modelsdevops.StatusSuccessful { - specHash := computeHash(copyPipeline.Spec) + specHash := utils.ComputeHash(copyPipeline.Spec) oldHash, _ := copyPipeline.Annotations[devopsv1alpha3.PipelineSpecHash] // don't need to check if it's nil, only compare if they're different if specHash == oldHash { // it was synced successfully, and there's any change with the Pipeline spec, skip this round @@ -319,30 +316,6 @@ func (c *Controller) syncHandler(key string) error { return nil } -func computeHash(obj interface{}) string { - hasher := fnv.New32a() - deepHashObject(hasher, obj) - return rand.SafeEncodeString(fmt.Sprint(hasher.Sum32())) -} - -// deepHashObject writes specified object to hash using the spew library -// which follows pointers and prints actual values of the nested objects -// ensuring the hash does not change when a pointer changes. -// **Notice** -// we don't want to import k8s.io/kubernetes as a module, but this is a very small function -// so just copy it from k8s.io/kubernetes@v1.14.0/pkg/util/hash/hash.go -// **Notice End** -func deepHashObject(hasher hash.Hash, objectToWrite interface{}) { - hasher.Reset() - printer := spew.ConfigState{ - Indent: " ", - SortKeys: true, - DisableMethods: true, - SpewKeys: true, - } - printer.Fprintf(hasher, "%#v", objectToWrite) -} - func isDevOpsProjectAdminNamespace(namespace *v1.Namespace) bool { _, ok := namespace.Labels[constants.DevOpsProjectLabelKey] diff --git a/pkg/controller/utils/hash.go b/pkg/controller/utils/hash.go new file mode 100644 index 000000000..8aba0fa3d --- /dev/null +++ b/pkg/controller/utils/hash.go @@ -0,0 +1,34 @@ +package utils + +import ( + "fmt" + "github.com/davecgh/go-spew/spew" + "hash" + "hash/fnv" + "k8s.io/apimachinery/pkg/util/rand" +) + +// ComputeHash computes hash value of a interface +func ComputeHash(obj interface{}) string { + hasher := fnv.New32a() + deepHashObject(hasher, obj) + return rand.SafeEncodeString(fmt.Sprint(hasher.Sum32())) +} + +// deepHashObject writes specified object to hash using the spew library +// which follows pointers and prints actual values of the nested objects +// ensuring the hash does not change when a pointer changes. +// **Notice** +// we don't want to import k8s.io/kubernetes as a module, but this is a very small function +// so just copy it from k8s.io/kubernetes@v1.14.0/pkg/util/hash/hash.go +// **Notice End** +func deepHashObject(hasher hash.Hash, objectToWrite interface{}) { + hasher.Reset() + printer := spew.ConfigState{ + Indent: " ", + SortKeys: true, + DisableMethods: true, + SpewKeys: true, + } + printer.Fprintf(hasher, "%#v", objectToWrite) +} From 054d85e6c46c013b19b67098bb735832f1931f94 Mon Sep 17 00:00:00 2001 From: rick Date: Mon, 1 Feb 2021 15:07:40 +0800 Subject: [PATCH 02/21] Fix the wrong count of total Pipeline Run number Signed-off-by: rick --- .../client/devops/jenkins/pure_request.go | 5 +- pkg/simple/client/devops/jenkins/utils.go | 25 +++----- .../client/devops/jenkins/utils_test.go | 57 +++++++++++++++++++ 3 files changed, 67 insertions(+), 20 deletions(-) create mode 100644 pkg/simple/client/devops/jenkins/utils_test.go diff --git a/pkg/simple/client/devops/jenkins/pure_request.go b/pkg/simple/client/devops/jenkins/pure_request.go index 89a28a871..78837d570 100644 --- a/pkg/simple/client/devops/jenkins/pure_request.go +++ b/pkg/simple/client/devops/jenkins/pure_request.go @@ -34,12 +34,13 @@ func (j *Jenkins) SendPureRequest(path string, httpParameters *devops.HttpParame // provider request header to call jenkins api. // transfer bearer token to basic token for inner Oauth and Jeknins func (j *Jenkins) SendPureRequestWithHeaderResp(path string, httpParameters *devops.HttpParameters) ([]byte, http.Header, error) { - Url, err := url.Parse(j.Server + path) + apiURL, err := url.Parse(j.Server + path) if err != nil { klog.Error(err) return nil, nil, err } + apiURL.RawQuery = httpParameters.Url.RawQuery client := &http.Client{Timeout: 30 * time.Second} header := httpParameters.Header @@ -47,7 +48,7 @@ func (j *Jenkins) SendPureRequestWithHeaderResp(path string, httpParameters *dev newRequest := &http.Request{ Method: httpParameters.Method, - URL: Url, + URL: apiURL, Header: header, Body: httpParameters.Body, Form: httpParameters.Form, diff --git a/pkg/simple/client/devops/jenkins/utils.go b/pkg/simple/client/devops/jenkins/utils.go index 10ea4c7e8..1987f778b 100644 --- a/pkg/simple/client/devops/jenkins/utils.go +++ b/pkg/simple/client/devops/jenkins/utils.go @@ -74,10 +74,9 @@ func getRespBody(resp *http.Response) ([]byte, error) { // ParseJenkinsQuery Parse the special query of jenkins. // ParseQuery in the standard library makes the query not re-encode -func ParseJenkinsQuery(query string) (url.Values, error) { - m := make(url.Values) - err := error(nil) - for query != "" { +func ParseJenkinsQuery(query string) (result url.Values, err error) { + result = make(url.Values) + for query != "" && err == nil { key := query if i := strings.IndexAny(key, "&"); i >= 0 { key, query = key[:i], key[i+1:] @@ -91,23 +90,13 @@ func ParseJenkinsQuery(query string) (url.Values, error) { if i := strings.Index(key, "="); i >= 0 { key, value = key[:i], key[i+1:] } - key, err1 := url.QueryUnescape(key) - if err1 != nil { - if err == nil { - err = err1 + if key, err = url.QueryUnescape(key); err == nil { + if value, err = url.QueryUnescape(value); err == nil { + result[key] = append(result[key], value) } - continue } - value, err1 = url.QueryUnescape(value) - if err1 != nil { - if err == nil { - err = err1 - } - continue - } - m[key] = append(m[key], value) } - return m, err + return } type JenkinsBlueTime time.Time diff --git a/pkg/simple/client/devops/jenkins/utils_test.go b/pkg/simple/client/devops/jenkins/utils_test.go new file mode 100644 index 000000000..71769173a --- /dev/null +++ b/pkg/simple/client/devops/jenkins/utils_test.go @@ -0,0 +1,57 @@ +package jenkins + +import ( + "github.com/stretchr/testify/assert" + "net/url" + "testing" +) + +func TestParseJenkinsQuery(t *testing.T) { + table := []testData{ + { + param: "start=0&limit=10&branch=master", + expected: url.Values{ + "start": []string{"0"}, + "limit": []string{"10"}, + "branch": []string{"master"}, + }, err: false, + }, + { + param: "branch=master", expected: url.Values{ + "branch": []string{"master"}, + }, err: false, + }, + { + param: "&branch=master", expected: url.Values{ + "branch": []string{"master"}, + }, err: false, + }, + { + param: "branch=master&", expected: url.Values{ + "branch": []string{"master"}, + }, err: false, + }, + { + param: "branch=%gg", expected: url.Values{}, err: true, + }, + { + param: "%gg=fake", expected: url.Values{}, err: true, + }, + } + + for index, item := range table { + result, err := parseJenkinsQuery(item.param) + if item.err { + assert.NotNil(t, err, "index: [%d], unexpected error happen %v", index, err) + } else { + assert.Nil(t, err, "index: [%d], unexpected error happen %v", index, err) + } + assert.Equal(t, item.expected, result, "index: [%d], result do not match with the expect value", index) + } +} + +type testData struct { + param string + expected interface{} + err bool +} From a5ec7b4b00e939cc0d259d63a56d5861642504f7 Mon Sep 17 00:00:00 2001 From: rick <1450685+LinuxSuRen@users.noreply.github.com> Date: Sat, 20 Feb 2021 10:15:03 +0800 Subject: [PATCH 03/21] Set a explicit loggger level in func SendPureRequestWithHeaderResp Signed-off-by: rick <1450685+LinuxSuRen@users.noreply.github.com> --- pkg/simple/client/devops/jenkins/pure_request.go | 2 +- pkg/simple/client/devops/jenkins/utils_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/simple/client/devops/jenkins/pure_request.go b/pkg/simple/client/devops/jenkins/pure_request.go index 78837d570..875cf67b7 100644 --- a/pkg/simple/client/devops/jenkins/pure_request.go +++ b/pkg/simple/client/devops/jenkins/pure_request.go @@ -36,7 +36,7 @@ func (j *Jenkins) SendPureRequest(path string, httpParameters *devops.HttpParame func (j *Jenkins) SendPureRequestWithHeaderResp(path string, httpParameters *devops.HttpParameters) ([]byte, http.Header, error) { apiURL, err := url.Parse(j.Server + path) if err != nil { - klog.Error(err) + klog.V(8).Info(err) return nil, nil, err } diff --git a/pkg/simple/client/devops/jenkins/utils_test.go b/pkg/simple/client/devops/jenkins/utils_test.go index 71769173a..e29379d68 100644 --- a/pkg/simple/client/devops/jenkins/utils_test.go +++ b/pkg/simple/client/devops/jenkins/utils_test.go @@ -40,7 +40,7 @@ func TestParseJenkinsQuery(t *testing.T) { } for index, item := range table { - result, err := parseJenkinsQuery(item.param) + result, err := ParseJenkinsQuery(item.param) if item.err { assert.NotNil(t, err, "index: [%d], unexpected error happen %v", index, err) } else { From c7e522afc26dd7e088cfa704de3bd2938ad412c4 Mon Sep 17 00:00:00 2001 From: Zhengyi Lai Date: Mon, 22 Feb 2021 14:51:05 +0800 Subject: [PATCH 04/21] Update OpenPitrix and Network roadmap Signed-off-by: Zhengyi Lai --- docs/roadmap.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index fa0571a88..ff96876d3 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -94,15 +94,12 @@ KubeSphere Roadmap demonstrates a list of open source product development plans ### Application Lifecycle Management (OpenPitrix) - [ ] Refactoring OpenPitrix with CRD, while fix bugs caused by legacy architecture [#3036](https://github.com/kubesphere/kubesphere/issues/3036) [#3001](https://github.com/kubesphere/kubesphere/issues/3001) [#2995](https://github.com/kubesphere/kubesphere/issues/2995) [#2981](https://github.com/kubesphere/kubesphere/issues/2981) [#2954](https://github.com/kubesphere/kubesphere/issues/2954) [#2951](https://github.com/kubesphere/kubesphere/issues/2951) [#2783](https://github.com/kubesphere/kubesphere/issues/2783) [#2713](https://github.com/kubesphere/kubesphere/issues/2713) [#2700](https://github.com/kubesphere/kubesphere/issues/2700) [#1903](https://github.com/kubesphere/kubesphere/issues/1903)  -- [ ] Support global repo [#1598](https://github.com/kubesphere/kubesphere/issues/1598) ### Network -- [ ] IPPool for Calico and VMs [#3057](https://github.com/kubesphere/kubesphere/issues/3057) -- [ ] Support for deployment using static IPs [#3058](https://github.com/kubesphere/kubesphere/issues/3058) -- [ ] Support for ks-installer with porter as a system component [#3059](https://github.com/kubesphere/kubesphere/issues/3059) -- [ ] Support for defining porter-related configuration items in the UI [#3060](https://github.com/kubesphere/kubesphere/issues/3060) -- [ ] Support network visualization [#3061](https://github.com/kubesphere/kubesphere/issues/3061) [#583](https://github.com/kubesphere/kubesphere/issues/583) +- [x] IPPool for Calico and VMs [#3057](https://github.com/kubesphere/kubesphere/issues/3057) +- [x] Support for deployment using static IPs [#3058](https://github.com/kubesphere/kubesphere/issues/3058) +- [x] Support network visualization [#3061](https://github.com/kubesphere/kubesphere/issues/3061) [#583](https://github.com/kubesphere/kubesphere/issues/583) ### Metering From 27eb3f7da600b32af208af62661cfa3ba5be900e Mon Sep 17 00:00:00 2001 From: Benjamin Date: Mon, 22 Feb 2021 15:43:34 +0800 Subject: [PATCH 05/21] Update roadmap for o11y & edge Signed-off-by: Benjamin --- docs/roadmap.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index fa0571a88..ebdaddd8c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -55,7 +55,7 @@ KubeSphere Roadmap demonstrates a list of open source product development plans ### KubeEdge Integration [#3070](https://github.com/kubesphere/kubesphere/issues/3070) -- [ ] KubeEdge cloud components setup. +- [x] KubeEdge cloud components setup. - [ ] KubeEdge edge nodes setup. - [x] Edge nodes logging and metrics support. - [x] Automatic network configuration on edge node joining/leaving. @@ -71,14 +71,14 @@ KubeSphere Roadmap demonstrates a list of open source product development plans - [x] Configure ServiceMonitor via UI. [#1031](https://github.com/kubesphere/console/pull/1301) - [x] PromQL auto-completion and syntax highlighting. [#1307](https://github.com/kubesphere/console/pull/1307) -- [ ] Support cluster-level custom monitoring. [#3193](https://github.com/kubesphere/kubesphere/pull/3193) -- [ ] Import dashboards from Grafana templates. +- [x] Support cluster-level custom monitoring. [#3193](https://github.com/kubesphere/kubesphere/pull/3193) +- [x] Tools to convert Grafana dashboard to KubeSphere Dashboard [#9](https://github.com/kubesphere/monitoring-dashboard/pull/9) #### Custom Alerting [#3065](https://github.com/kubesphere/kubesphere/issues/3065) -- [ ] Prometheus alert rule management. [#3181](https://github.com/kubesphere/kubesphere/pull/3181) -- [ ] Alert rule tenant control: global/namespace level alert rules. [#3181](https://github.com/kubesphere/kubesphere/pull/3181) -- [ ] List alerts for a specific alert rule. [#3181](https://github.com/kubesphere/kubesphere/pull/3181) +- [x] Prometheus alert rule management. [#3181](https://github.com/kubesphere/kubesphere/pull/3181) +- [x] Alert rule tenant control: global/namespace level alert rules. [#3181](https://github.com/kubesphere/kubesphere/pull/3181) +- [x] List alerts for a specific alert rule. [#3181](https://github.com/kubesphere/kubesphere/pull/3181) #### Multi-tenant Notification support including Email/DingTalk/Slack/Wechat works/Webhook [#3066](https://github.com/kubesphere/kubesphere/issues/3066) @@ -132,6 +132,7 @@ KubeSphere Roadmap demonstrates a list of open source product development plans - [ ] Support adding member clusters for an existing project(ns), wich can be helpful when there are new clusters joined. [#3246](https://github.com/kubesphere/kubesphere/issues/3246) ### Observability +- [ ] Upgrade to Prometheus v2.25.0 - [x] Upgrade Notification Manager to v0.7.0+ [Releases](https://github.com/kubesphere/notification-manager/releases) - [x] Upgrade FluentBit Operator to v0.3.0+ [Releases](https://github.com/kubesphere/fluentbit-operator/releases) - [ ] Upgrade FluentBit to v1.6.9+ From 78c7b81ce5e0ebcd54e422967360d46540cc836e Mon Sep 17 00:00:00 2001 From: Duan Jiong Date: Tue, 23 Feb 2021 11:25:36 +0800 Subject: [PATCH 06/21] Don't exit while synchronizing calico ippool Signed-off-by: Duan Jiong --- pkg/controller/network/ippool/ippool_controller.go | 8 +++++--- pkg/simple/client/network/ippool/calico/provider.go | 13 ++++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pkg/controller/network/ippool/ippool_controller.go b/pkg/controller/network/ippool/ippool_controller.go index 85c0ed5ee..bbf1fe7ce 100644 --- a/pkg/controller/network/ippool/ippool_controller.go +++ b/pkg/controller/network/ippool/ippool_controller.go @@ -339,12 +339,14 @@ func (c *IPPoolController) processIPPoolItem() bool { if err == nil { c.ippoolQueue.Forget(key) return true - } else if delay != nil { - c.ippoolQueue.AddAfter(key, *delay) } + if delay != nil { + c.ippoolQueue.AddAfter(key, *delay) + } else { + c.ippoolQueue.AddRateLimited(key) + } utilruntime.HandleError(fmt.Errorf("error processing ippool %v (will retry): %v", key, err)) - c.ippoolQueue.AddRateLimited(key) return true } diff --git a/pkg/simple/client/network/ippool/calico/provider.go b/pkg/simple/client/network/ippool/calico/provider.go index 4cb5c5846..b4d622208 100644 --- a/pkg/simple/client/network/ippool/calico/provider.go +++ b/pkg/simple/client/network/ippool/calico/provider.go @@ -695,9 +695,16 @@ func NewProvider(podInformer informercorev1.PodInformer, ksclient kubesphereclie }) p.block = blockI - if err := p.syncIPPools(); err != nil { - klog.Fatalf("failed to sync calico ippool to kubesphere ippool, err=%v", err) - } + go func() { + for { + if err := p.syncIPPools(); err != nil { + klog.Infof("failed to sync calico ippool to kubesphere ippool, err=%v", err) + time.Sleep(3 * time.Second) + continue + } + break + } + }() return p } From 194d0549738bbeafc72bf2ab714e53d107a5f6fa Mon Sep 17 00:00:00 2001 From: yuswift Date: Tue, 23 Feb 2021 16:24:22 +0800 Subject: [PATCH 07/21] refactor cluster controller Signed-off-by: yuswift --- pkg/controller/cluster/cluster_controller.go | 313 ++++++------------- pkg/simple/client/multicluster/options.go | 4 +- 2 files changed, 97 insertions(+), 220 deletions(-) diff --git a/pkg/controller/cluster/cluster_controller.go b/pkg/controller/cluster/cluster_controller.go index c138e6cae..500d22653 100644 --- a/pkg/controller/cluster/cluster_controller.go +++ b/pkg/controller/cluster/cluster_controller.go @@ -21,7 +21,6 @@ import ( "context" "encoding/json" "fmt" - "math/rand" "net/http" "reflect" "sync" @@ -33,7 +32,6 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/util/intstr" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/wait" @@ -44,6 +42,7 @@ import ( "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/retry" "k8s.io/client-go/util/workqueue" "k8s.io/klog" fedv1b1 "sigs.k8s.io/kubefed/pkg/apis/core/v1beta1" @@ -56,12 +55,12 @@ import ( ) // Cluster controller only runs under multicluster mode. Cluster controller is following below steps, -// 1. Populates proxy spec if cluster connection type is proxy -// 1.1 Wait for cluster agent is ready if connection type is proxy +// 1. Wait for cluster agent is ready if connection type is proxy // 2. Join cluster into federation control plane if kubeconfig is ready. // 3. Pull cluster version and configz, set result to cluster status // Also put all clusters back into queue every 5 * time.Minute to sync cluster status, this is needed // in case there aren't any cluster changes made. +// Also check if all of the clusters are ready by the spec.connection.kubeconfig every resync period const ( // maxRetries is the number of times a service will be retried before it is dropped out of the queue. @@ -83,17 +82,14 @@ const ( portRangeMin = 6000 portRangeMax = 7000 - // Proxy service port - kubernetesPort = 6443 - kubespherePort = 80 - - defaultAgentNamespace = "kubesphere-system" - // proxy format proxyFormat = "%s/api/v1/namespaces/kubesphere-system/services/:ks-apiserver:80/proxy/%s" // mulitcluster configuration name configzMultiCluster = "multicluster" + + // probe cluster timeout + probeClusterTimeout = 3 * time.Second ) // Cluster template for reconcile host cluster if there is none. @@ -223,12 +219,16 @@ func (c *clusterController) Run(workers int, stopCh <-chan struct{}) error { go wait.Until(c.worker, c.workerLoopPeriod, stopCh) } - // refresh cluster configz every 2 minutes + // refresh cluster configz every resync period go wait.Until(func() { if err := c.reconcileHostCluster(); err != nil { klog.Errorf("Error create host cluster, error %v", err) } + if err := c.probeClusters(); err != nil { + klog.Errorf("failed to reconcile cluster ready status, err: %v", err) + } + }, c.resyncPeriod, stopCh) <-stopCh @@ -348,6 +348,80 @@ func (c *clusterController) reconcileHostCluster() error { return err } +func (c *clusterController) probeClusters() error { + clusters, err := c.clusterLister.List(labels.Everything()) + if err != nil { + return err + } + + for _, cluster := range clusters { + if len(cluster.Spec.Connection.KubeConfig) == 0 { + continue + } + + clientConfig, err := clientcmd.NewClientConfigFromBytes(cluster.Spec.Connection.KubeConfig) + if err != nil { + klog.Error(err) + continue + } + + config, err := clientConfig.ClientConfig() + if err != nil { + klog.Error(err) + continue + } + config.Timeout = probeClusterTimeout + + clientSet, err := kubernetes.NewForConfig(config) + if err != nil { + klog.Error(err) + continue + } + + var con clusterv1alpha1.ClusterCondition + _, err = clientSet.Discovery().ServerVersion() + if err == nil { + con = clusterv1alpha1.ClusterCondition{ + Type: clusterv1alpha1.ClusterReady, + Status: v1.ConditionTrue, + LastUpdateTime: metav1.Now(), + LastTransitionTime: metav1.Now(), + Reason: string(clusterv1alpha1.ClusterReady), + Message: "Cluster is available now", + } + } else { + con = clusterv1alpha1.ClusterCondition{ + Type: clusterv1alpha1.ClusterReady, + Status: v1.ConditionFalse, + LastUpdateTime: metav1.Now(), + LastTransitionTime: metav1.Now(), + Reason: "failed to connect get kubernetes version", + Message: "Cluster is not available now", + } + } + + c.updateClusterCondition(cluster, con) + err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { + ct, err := c.clusterClient.Get(context.TODO(), cluster.Name, metav1.GetOptions{}) + if err != nil { + return err + } + + ct.Status.Conditions = cluster.Status.Conditions + ct, err = c.clusterClient.Update(context.TODO(), ct, metav1.UpdateOptions{}) + return err + }) + if err != nil { + klog.Errorf("failed to update cluster %s status, err: %v", cluster.Name, err) + } else { + klog.V(4).Infof("successfully updated cluster %s to status %v", cluster.Name, con) + } + + } + + return nil +} + func (c *clusterController) syncCluster(key string) error { klog.V(5).Infof("starting to sync cluster %s", key) startTime := time.Now() @@ -363,6 +437,7 @@ func (c *clusterController) syncCluster(key string) error { }() cluster, err := c.clusterLister.Get(name) + if err != nil { // cluster not found, possibly been deleted // need to do the cleanup @@ -374,9 +449,6 @@ func (c *clusterController) syncCluster(key string) error { return err } - // proxy service name if needed - serviceName := fmt.Sprintf("mc-%s", cluster.Name) - if cluster.ObjectMeta.DeletionTimestamp.IsZero() { // The object is not being deleted, so if it does not have our finalizer, // then lets add the finalizer and update the object. This is equivalent @@ -399,22 +471,6 @@ func (c *clusterController) syncCluster(key string) error { return err } - _, err = c.client.CoreV1().Services(defaultAgentNamespace).Get(context.TODO(), serviceName, metav1.GetOptions{}) - if err != nil { - if errors.IsNotFound(err) { - // nothing to do - } else { - klog.Errorf("Failed to get proxy service %s, error %v", serviceName, err) - return err - } - } else { - err = c.client.CoreV1().Services(defaultAgentNamespace).Delete(context.TODO(), serviceName, *metav1.NewDeleteOptions(0)) - if err != nil { - klog.Errorf("Unable to delete service %s, error %v", serviceName, err) - return err - } - } - // clean up openpitrix runtime of the cluster if _, ok := cluster.Annotations[openpitrixRuntime]; ok { if c.openpitrixClient != nil { @@ -438,136 +494,18 @@ func (c *clusterController) syncCluster(key string) error { return nil } + // save a old copy of cluster + oldCluster := cluster.DeepCopy() + // currently we didn't set cluster.Spec.Enable when creating cluster at client side, so only check // if we enable cluster.Spec.JoinFederation now if cluster.Spec.JoinFederation == false { return nil } - // save a old copy of cluster - oldCluster := cluster.DeepCopy() - - // prepare for proxy to member cluster - if cluster.Spec.Connection.Type == clusterv1alpha1.ConnectionTypeProxy { - - // allocate ports for kubernetes and kubesphere endpoint - if cluster.Spec.Connection.KubeSphereAPIServerPort == 0 || - cluster.Spec.Connection.KubernetesAPIServerPort == 0 { - port, err := c.allocatePort() - if err != nil { - klog.Error(err) - return err - } - - cluster.Spec.Connection.KubernetesAPIServerPort = port - cluster.Spec.Connection.KubeSphereAPIServerPort = port + 10000 - } - - // token uninitialized, generate a new token - if len(cluster.Spec.Connection.Token) == 0 { - cluster.Spec.Connection.Token = c.generateToken() - } - - // create a proxy service spec - mcService := v1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: serviceName, - Namespace: cluster.Namespace, - Labels: map[string]string{ - "app.kubernetes.io/name": serviceName, - "app": serviceName, - }, - }, - Spec: v1.ServiceSpec{ - Selector: map[string]string{ - "app.kubernetes.io/name": "tower", - "app": "tower", - }, - Ports: []v1.ServicePort{ - { - Name: "kubernetes", - Protocol: v1.ProtocolTCP, - Port: kubernetesPort, - TargetPort: intstr.FromInt(int(cluster.Spec.Connection.KubernetesAPIServerPort)), - }, - { - Name: "kubesphere", - Protocol: v1.ProtocolTCP, - Port: kubespherePort, - TargetPort: intstr.FromInt(int(cluster.Spec.Connection.KubeSphereAPIServerPort)), - }, - }, - }, - } - - service, err := c.client.CoreV1().Services(defaultAgentNamespace).Get(context.TODO(), serviceName, metav1.GetOptions{}) - if err != nil { // proxy service not found - if errors.IsNotFound(err) { - service, err = c.client.CoreV1().Services(defaultAgentNamespace).Create(context.TODO(), &mcService, metav1.CreateOptions{}) - if err != nil { - return err - } - } - - return err - } else { // update existed proxy service - if !reflect.DeepEqual(service.Spec, mcService.Spec) { - mcService.ObjectMeta = service.ObjectMeta - mcService.Spec.ClusterIP = service.Spec.ClusterIP - - service, err = c.client.CoreV1().Services(defaultAgentNamespace).Update(context.TODO(), &mcService, metav1.UpdateOptions{}) - if err != nil { - return err - } - } - } - - // populates the kubernetes apiEndpoint and kubesphere apiEndpoint - cluster.Spec.Connection.KubernetesAPIEndpoint = fmt.Sprintf("https://%s:%d", service.Spec.ClusterIP, kubernetesPort) - cluster.Spec.Connection.KubeSphereAPIEndpoint = fmt.Sprintf("http://%s:%d", service.Spec.ClusterIP, kubespherePort) - - initializedCondition := clusterv1alpha1.ClusterCondition{ - Type: clusterv1alpha1.ClusterInitialized, - Status: v1.ConditionTrue, - Reason: string(clusterv1alpha1.ClusterInitialized), - Message: "Cluster has been initialized", - LastUpdateTime: metav1.Now(), - LastTransitionTime: metav1.Now(), - } - - if !isConditionTrue(cluster, clusterv1alpha1.ClusterInitialized) { - c.updateClusterCondition(cluster, initializedCondition) - } - - if !reflect.DeepEqual(oldCluster, cluster) { - cluster, err = c.clusterClient.Update(context.TODO(), cluster, metav1.UpdateOptions{}) - if err != nil { - klog.Errorf("Error updating cluster %s, error %s", cluster.Name, err) - return err - } - } - } - - // agent status unavailable, which means the agent disconnected from the server or has not connected to the server - // we need to update the cluster ready status unavailable and return. - if cluster.Spec.Connection.Type == clusterv1alpha1.ConnectionTypeProxy && - !isConditionTrue(cluster, clusterv1alpha1.ClusterAgentAvailable) { - clusterNotReadyCondition := clusterv1alpha1.ClusterCondition{ - Type: clusterv1alpha1.ClusterReady, - Status: v1.ConditionFalse, - LastUpdateTime: metav1.Now(), - LastTransitionTime: metav1.Now(), - Reason: "Unable to establish connection with cluster", - Message: "Cluster is not available now", - } - - c.updateClusterCondition(cluster, clusterNotReadyCondition) - - cluster, err = c.clusterClient.Update(context.TODO(), cluster, metav1.UpdateOptions{}) - if err != nil { - klog.Errorf("Error updating cluster %s, error %s", cluster.Name, err) - } - return err + // cluster not ready, nothing to do + if !isConditionTrue(cluster, clusterv1alpha1.ClusterReady) { + return nil } // build up cached cluster data if there isn't any @@ -594,10 +532,10 @@ func (c *clusterController) syncCluster(key string) error { _, err = c.joinFederation(clusterDt.config, cluster.Name, cluster.Labels) if err != nil { klog.Errorf("Failed to join federation for cluster %s, error %v", cluster.Name, err) - c.eventRecorder.Event(cluster, v1.EventTypeWarning, "JoinFederation", err.Error()) return err } - c.eventRecorder.Event(cluster, v1.EventTypeNormal, "JoinFederation", "Cluster has joined federation.") + + klog.Infof("successfully joined federation for cluster %s", cluster.Name) federationReadyCondition := clusterv1alpha1.ClusterCondition{ Type: clusterv1alpha1.ClusterFederated, @@ -611,7 +549,7 @@ func (c *clusterController) syncCluster(key string) error { c.updateClusterCondition(cluster, federationReadyCondition) } - // cluster agent is ready, we can pull kubernetes cluster info through agent + // cluster is ready, we can pull kubernetes cluster info through agent // since there is no agent necessary for host cluster, so updates for host cluster // is safe. if len(cluster.Spec.Connection.KubernetesAPIEndpoint) == 0 { @@ -647,17 +585,6 @@ func (c *clusterController) syncCluster(key string) error { cluster.Labels[clusterv1alpha1.HostCluster] = "" } - clusterReadyCondition := clusterv1alpha1.ClusterCondition{ - Type: clusterv1alpha1.ClusterReady, - Status: v1.ConditionTrue, - LastUpdateTime: metav1.Now(), - LastTransitionTime: metav1.Now(), - Reason: string(clusterv1alpha1.ClusterReady), - Message: "Cluster is available now", - } - - c.updateClusterCondition(cluster, clusterReadyCondition) - if c.openpitrixClient != nil { // OpenPitrix is enabled, create runtime if cluster.GetAnnotations() == nil { cluster.Annotations = make(map[string]string) @@ -746,16 +673,6 @@ func (c *clusterController) addCluster(obj interface{}) { c.queue.Add(key) } -func hasHostClusterLabel(cluster *clusterv1alpha1.Cluster) bool { - if cluster.Labels == nil || len(cluster.Labels) == 0 { - return false - } - - _, ok := cluster.Labels[clusterv1alpha1.HostCluster] - - return ok -} - func (c *clusterController) handleErr(err error, key interface{}) { if err == nil { c.queue.Forget(key) @@ -855,43 +772,3 @@ func (c *clusterController) unJoinFederation(clusterConfig *rest.Config, unjoini } } } - -// allocatePort find a available port between [portRangeMin, portRangeMax] in maximumRetries -// TODO: only works with handful clusters -func (c *clusterController) allocatePort() (uint16, error) { - rand.Seed(time.Now().UnixNano()) - - clusters, err := c.clusterLister.List(labels.Everything()) - if err != nil { - return 0, err - } - - const maximumRetries = 10 - for i := 0; i < maximumRetries; i++ { - collision := false - port := uint16(portRangeMin + rand.Intn(portRangeMax-portRangeMin+1)) - - for _, item := range clusters { - if item.Spec.Connection.Type == clusterv1alpha1.ConnectionTypeProxy && - item.Spec.Connection.KubernetesAPIServerPort != 0 && - item.Spec.Connection.KubeSphereAPIServerPort == port { - collision = true - break - } - } - - if !collision { - return port, nil - } - } - - return 0, fmt.Errorf("unable to allocate port after %d retries", maximumRetries) -} - -// generateToken returns a random 32-byte string as token -func (c *clusterController) generateToken() string { - rand.Seed(time.Now().UnixNano()) - b := make([]byte, 32) - rand.Read(b) - return fmt.Sprintf("%x", b) -} diff --git a/pkg/simple/client/multicluster/options.go b/pkg/simple/client/multicluster/options.go index 7c31cc3fd..c6ece89fd 100644 --- a/pkg/simple/client/multicluster/options.go +++ b/pkg/simple/client/multicluster/options.go @@ -22,7 +22,7 @@ import ( "github.com/spf13/pflag" ) -const DefaultResyncPeriod = time.Duration(120) * time.Second +const DefaultResyncPeriod = 120 * time.Second type Options struct { // Enable @@ -79,5 +79,5 @@ func (o *Options) AddFlags(fs *pflag.FlagSet, s *Options) { "This field is used when generating deployment yaml for agent.") fs.DurationVar(&o.ClusterControllerResyncSecond, "cluster-controller-resync-second", s.ClusterControllerResyncSecond, - "Cluster controller resync second to sync cluster resource.") + "Cluster controller resync second to sync cluster resource. e.g. 2m 5m 10m ... default set to 2m") } From f6bcdc50fdb8f125f6785880909f28c96ead51bc Mon Sep 17 00:00:00 2001 From: shaowenchen Date: Wed, 24 Feb 2021 14:19:09 +0800 Subject: [PATCH 08/21] feature: add annotations to pipelines for getting syncstatus Signed-off-by: shaowenchen --- .gitignore | 1 + pkg/kapis/devops/v1alpha2/devops.go | 3 ++- pkg/simple/client/devops/pipeline.go | 5 +++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 25c28ed9a..fffd01e33 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ bin/ # Vscode files .vscode/ +__debug_bin # OSX trash .DS_Store diff --git a/pkg/kapis/devops/v1alpha2/devops.go b/pkg/kapis/devops/v1alpha2/devops.go index e5c6c93f1..d845e0b52 100644 --- a/pkg/kapis/devops/v1alpha2/devops.go +++ b/pkg/kapis/devops/v1alpha2/devops.go @@ -112,7 +112,8 @@ func (h *ProjectPipelineHandler) ListPipelines(req *restful.Request, resp *restf } else { pipelineMap[pipeline.Name] = i pipelineList.Items[i] = clientDevOps.Pipeline{ - Name: pipeline.Name, + Name: pipeline.Name, + Annotations: pipeline.Annotations, } } } diff --git a/pkg/simple/client/devops/pipeline.go b/pkg/simple/client/devops/pipeline.go index 0260db4bf..00e7c5405 100644 --- a/pkg/simple/client/devops/pipeline.go +++ b/pkg/simple/client/devops/pipeline.go @@ -31,8 +31,9 @@ type PipelineList struct { // GetPipeline & SearchPipelines type Pipeline struct { - Class string `json:"_class,omitempty" description:"It’s a fully qualified name and is an identifier of the producer of this resource's capability." ` - Links struct { + Annotations map[string]string `json:"annotations,omitempty" description:"Add annotations from crd" ` + Class string `json:"_class,omitempty" description:"It’s a fully qualified name and is an identifier of the producer of this resource's capability." ` + Links struct { Self struct { Class string `json:"_class,omitempty"` Href string `json:"href,omitempty"` From 7aa602e12498c1543c41979b385660fb68be4d0d Mon Sep 17 00:00:00 2001 From: "Roland.Ma" Date: Wed, 24 Feb 2021 06:56:23 +0000 Subject: [PATCH 09/21] e2e github action Signed-off-by: Roland.Ma --- .github/workflows/e2e-test.yml | 37 +++++++++++++++++++++ .github/workflows/kind/kind.yaml | 11 +++++++ hack/deploy-kubesphere.sh | 56 ++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 .github/workflows/e2e-test.yml create mode 100644 .github/workflows/kind/kind.yaml create mode 100755 hack/deploy-kubesphere.sh diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml new file mode 100644 index 000000000..78bb9f7e8 --- /dev/null +++ b/.github/workflows/e2e-test.yml @@ -0,0 +1,37 @@ +name: e2e + +on: + schedule: + # run e2e test every 4 hours + - cron: 0 */4 * * * + workflow_dispatch: +jobs: + build: + name: Test + runs-on: ubuntu-latest + env: + GO111MODULE: on + steps: + + - name: Set up Go 1.13 + uses: actions/setup-go@v1 + with: + go-version: 1.13 + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + + - name: Downloading go dependencies + run: go mod vendor + + - name: Create kind cluster + uses: helm/kind-action@v1.0.0-rc.1 + with: + config: .github/workflows/kind/kind.yaml + + - name: Deploy KubeSphere to Kind + run: KIND_CLUSTER_NAME=chart-testing hack/deploy-kubesphere.sh + + - name: Run e2e testing + run: go test ./test/e2e \ No newline at end of file diff --git a/.github/workflows/kind/kind.yaml b/.github/workflows/kind/kind.yaml new file mode 100644 index 000000000..4179d133b --- /dev/null +++ b/.github/workflows/kind/kind.yaml @@ -0,0 +1,11 @@ +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: +- role: control-plane + image: kindest/node:v1.19.7 + extraMounts: + - hostPath: /etc/localtime + containerPath: /etc/localtime + extraPortMappings: + - containerPort: 30881 + hostPort: 9090 \ No newline at end of file diff --git a/hack/deploy-kubesphere.sh b/hack/deploy-kubesphere.sh new file mode 100755 index 000000000..b1d5c192d --- /dev/null +++ b/hack/deploy-kubesphere.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +# Copyright 2020 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. + +set -o errexit +set -o nounset +set -o pipefail + +function wait_for_installation_finish() { + echo "waiting for ks-installer pod ready" + kubectl -n kubesphere-system wait --timeout=180s --for=condition=Ready $(kubectl -n kubesphere-system get pod -l app=ks-install -oname) + echo "waiting for KubeSphere ready" + while IFS= read -r line; do + if [[ $line =~ "Welcome to KubeSphere" ]] + then + break + fi + done < <(timeout 900 kubectl logs -n kubesphere-system deploy/ks-installer -f) +} + +# Use kubespheredev and latest tag as default image +TAG="${TAG:-latest}" +REPO="${REPO:-kubespheredev}" + +# Use KIND_LOAD_IMAGE=y .hack/deploy-kubesphere.sh to load +# the built docker image into kind before deploying. +if [[ "${KIND_LOAD_IMAGE:-}" == "y" ]]; then + kind load docker-image "$REPO/ks-apiserver:$TAG" --name="${KIND_CLUSTER_NAME:-kind}" + kind load docker-image "$REPO/ks-controller-manager:$TAG" --name="${KIND_CLUSTER_NAME:-kind}" +fi + +# Download the latest ks-install to deploy KubeSphere +wget --retry-connrefused --waitretry=1 --read-timeout=20 --timeout=15 --tries 3 https://raw.githubusercontent.com/kubesphere/ks-installer/master/deploy/kubesphere-installer.yaml +wget --retry-connrefused --waitretry=1 --read-timeout=20 --timeout=15 --tries 3 https://raw.githubusercontent.com/kubesphere/ks-installer/master/deploy/cluster-configuration.yaml + +#TODO: override ks-apiserver and ks-controller-manager images with specific tag + +kubectl apply -f kubesphere-installer.yaml +kubectl apply -f cluster-configuration.yaml + +wait_for_installation_finish + +# Expose KubeSphere API Server +kubectl -n kubesphere-system patch svc ks-apiserver -p '{"spec":{"type":"NodePort","ports":[{"name":"ks-apiserver","port":80,"protocal":"TCP","targetPort":9090,"nodePort":30881}]}}' From 2705d25e8344ccd005c936f9fa8070e5edae7787 Mon Sep 17 00:00:00 2001 From: Duan Jiong Date: Wed, 24 Feb 2021 18:06:36 +0800 Subject: [PATCH 10/21] Update calico lib fix struct definition error in v3 Signed-off-by: Duan Jiong --- go.mod | 9 +++-- go.sum | 5 ++- .../lib/apis/v3/globalnetworkpolicy.go | 2 +- .../libcalico-go/lib/apis/v3/ipam_block.go | 2 +- .../libcalico-go/lib/apis/v3/networkpolicy.go | 2 +- .../lib/backend/k8s/conversion/conversion.go | 36 +++++++++++++++++-- .../libcalico-go/lib/backend/model/block.go | 2 +- vendor/modules.txt | 2 +- 8 files changed, 44 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index b683c8bdb..942e86631 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pkg/errors v0.9.1 github.com/projectcalico/kube-controllers v3.8.8+incompatible - github.com/projectcalico/libcalico-go v1.7.2-0.20191104213956-8f81e1e344ce + github.com/projectcalico/libcalico-go v1.7.2-0.20191014160346-2382c6cdd056 github.com/prometheus-community/prom-label-proxy v0.2.0 github.com/prometheus-operator/prometheus-operator v0.42.2-0.20200928114327-fbd01683839a github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.42.1 @@ -98,12 +98,12 @@ require ( k8s.io/kubectl v0.18.6 k8s.io/metrics v0.18.6 k8s.io/utils v0.0.0-20200603063816-c1c6865ac451 + kubesphere.io/client-go v0.0.0 openpitrix.io/openpitrix v0.4.9-0.20200611125425-ae07f141e797 sigs.k8s.io/application v0.8.4-0.20201016185654-c8e2959e57a0 sigs.k8s.io/controller-runtime v0.6.4 sigs.k8s.io/controller-tools v0.4.0 sigs.k8s.io/kubefed v0.4.0 - kubesphere.io/client-go v0.0.0 ) replace ( @@ -559,7 +559,7 @@ replace ( github.com/projectcalico/go-yaml => github.com/projectcalico/go-yaml v0.0.0-20161201183616-955bc3e451ef github.com/projectcalico/go-yaml-wrapper => github.com/projectcalico/go-yaml-wrapper v0.0.0-20161127220527-598e54215bee github.com/projectcalico/kube-controllers => github.com/projectcalico/kube-controllers v3.8.8+incompatible - github.com/projectcalico/libcalico-go => github.com/projectcalico/libcalico-go v1.7.2-0.20191104213956-8f81e1e344ce + github.com/projectcalico/libcalico-go => github.com/projectcalico/libcalico-go v1.7.2-0.20191014160346-2382c6cdd056 github.com/prometheus-community/prom-label-proxy => github.com/prometheus-community/prom-label-proxy v0.2.0 github.com/prometheus-operator/prometheus-operator => github.com/prometheus-operator/prometheus-operator v0.42.2-0.20200928114327-fbd01683839a github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring => github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.42.1 @@ -735,6 +735,7 @@ replace ( k8s.io/kubectl => k8s.io/kubectl v0.18.6 k8s.io/metrics => k8s.io/metrics v0.18.6 k8s.io/utils => k8s.io/utils v0.0.0-20200603063816-c1c6865ac451 + kubesphere.io/client-go => ./staging/src/kubesphere.io/client-go kubesphere.io/im => kubesphere.io/im v0.1.0 openpitrix.io/iam => openpitrix.io/iam v0.1.0 openpitrix.io/libqueue => openpitrix.io/libqueue v0.4.1 @@ -758,6 +759,4 @@ replace ( sigs.k8s.io/yaml => sigs.k8s.io/yaml v1.2.0 sourcegraph.com/sourcegraph/appdash => sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0 vbom.ml/util => vbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc - - kubesphere.io/client-go => ./staging/src/kubesphere.io/client-go ) diff --git a/go.sum b/go.sum index ba15f7f70..1cd294de9 100644 --- a/go.sum +++ b/go.sum @@ -596,8 +596,8 @@ github.com/projectcalico/go-yaml-wrapper v0.0.0-20161127220527-598e54215bee h1:y github.com/projectcalico/go-yaml-wrapper v0.0.0-20161127220527-598e54215bee/go.mod h1:UgC0aTQ2KMDxlX3lU/stndk7DMUBJqzN40yFiILHgxc= github.com/projectcalico/kube-controllers v3.8.8+incompatible h1:ZbCg0wJ+gd7i81CB6vOASiUN//oR4ZBl+wEdy0Vk1uI= github.com/projectcalico/kube-controllers v3.8.8+incompatible/go.mod h1:ZEafKeKN5wiNARRw1LZP8l10uEfp04C7redU848MMZw= -github.com/projectcalico/libcalico-go v1.7.2-0.20191104213956-8f81e1e344ce h1:O/R67iwUe8TvZwgKbDB2cvF2/8L8PR4zVOcBtYEHD5Y= -github.com/projectcalico/libcalico-go v1.7.2-0.20191104213956-8f81e1e344ce/go.mod h1:z4tuFqrAg/423AMSaDamY5LgqeOZ5ETui6iOxDwJ/ag= +github.com/projectcalico/libcalico-go v1.7.2-0.20191014160346-2382c6cdd056 h1:qs29Hus4cY8XlsmMLUsSAHT0metSTyqu2Tnpuwy5dkM= +github.com/projectcalico/libcalico-go v1.7.2-0.20191014160346-2382c6cdd056/go.mod h1:tUt8rirmysRy7TR1S80XDriwBK1z2igwwX79lnUrSf4= github.com/prometheus-community/prom-label-proxy v0.2.0 h1:2cNKhNjbTmmEDvBTW/6WUsE2x7bh76rBMZVBn4ey6To= github.com/prometheus-community/prom-label-proxy v0.2.0/go.mod h1:XdjyZg7LCbCC5FADHtpgNp6kQ0W9beXVGfmcvndMj5Y= github.com/prometheus-operator/prometheus-operator v0.42.2-0.20200928114327-fbd01683839a h1:21yBrtc90hdEhJaL815CHNV0GW0DEdgxDOiL5OYoSHo= @@ -812,7 +812,6 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= diff --git a/vendor/github.com/projectcalico/libcalico-go/lib/apis/v3/globalnetworkpolicy.go b/vendor/github.com/projectcalico/libcalico-go/lib/apis/v3/globalnetworkpolicy.go index 7cfd10abf..131a30e9e 100644 --- a/vendor/github.com/projectcalico/libcalico-go/lib/apis/v3/globalnetworkpolicy.go +++ b/vendor/github.com/projectcalico/libcalico-go/lib/apis/v3/globalnetworkpolicy.go @@ -90,7 +90,7 @@ type GlobalNetworkPolicySpec struct { // type in {"frontend", "backend"} // deployment != "dev" // ! has(label_name) - Selector string `json:"selector,omitempty" validate:"selector"` + Selector string `json:"selector" validate:"selector"` // Types indicates whether this policy applies to ingress, or to egress, or to both. When // not explicitly specified (and so the value on creation is empty or nil), Calico defaults // Types according to what Ingress and Egress rules are present in the policy. The diff --git a/vendor/github.com/projectcalico/libcalico-go/lib/apis/v3/ipam_block.go b/vendor/github.com/projectcalico/libcalico-go/lib/apis/v3/ipam_block.go index 745fd4ac8..e0a6dbc1e 100644 --- a/vendor/github.com/projectcalico/libcalico-go/lib/apis/v3/ipam_block.go +++ b/vendor/github.com/projectcalico/libcalico-go/lib/apis/v3/ipam_block.go @@ -43,7 +43,7 @@ type IPAMBlockSpec struct { Allocations []*int `json:"allocations"` Unallocated []int `json:"unallocated"` Attributes []AllocationAttribute `json:"attributes"` - Deleted bool `json:"deleted` + Deleted bool `json:"deleted"` } type AllocationAttribute struct { diff --git a/vendor/github.com/projectcalico/libcalico-go/lib/apis/v3/networkpolicy.go b/vendor/github.com/projectcalico/libcalico-go/lib/apis/v3/networkpolicy.go index f204acbbe..6e5dcec14 100644 --- a/vendor/github.com/projectcalico/libcalico-go/lib/apis/v3/networkpolicy.go +++ b/vendor/github.com/projectcalico/libcalico-go/lib/apis/v3/networkpolicy.go @@ -71,7 +71,7 @@ type NetworkPolicySpec struct { // type in {"frontend", "backend"} // deployment != "dev" // ! has(label_name) - Selector string `json:"selector,omitempty" validate:"selector"` + Selector string `json:"selector" validate:"selector"` // Types indicates whether this policy applies to ingress, or to egress, or to both. When // not explicitly specified (and so the value on creation is empty or nil), Calico defaults // Types according to what Ingress and Egress are present in the policy. The diff --git a/vendor/github.com/projectcalico/libcalico-go/lib/backend/k8s/conversion/conversion.go b/vendor/github.com/projectcalico/libcalico-go/lib/backend/k8s/conversion/conversion.go index bf71aee19..8b0c9dec2 100644 --- a/vendor/github.com/projectcalico/libcalico-go/lib/backend/k8s/conversion/conversion.go +++ b/vendor/github.com/projectcalico/libcalico-go/lib/backend/k8s/conversion/conversion.go @@ -535,15 +535,45 @@ func (c Converter) k8sRuleToCalico(rPeers []networkingv1.NetworkPolicyPeer, rPor ports = []*networkingv1.NetworkPolicyPort{nil} } - // Combine destinations with sources to generate rules. - // TODO: This currently creates a lot of rules by making every combination of from / ports - // into a rule. We can combine these so that we don't need as many rules! + protocolPorts := map[string][]numorstring.Port{} + for _, port := range ports { protocol, calicoPorts, err := c.k8sPortToCalicoFields(port) if err != nil { return nil, fmt.Errorf("failed to parse k8s port: %s", err) } + // These are either both present or both nil + if protocol == nil && calicoPorts == nil { + // If nil, no ports were specified, or an empty port struct was provided, which we translate to allowing all. + // We want to use a nil protocol and a nil list of ports, which will allow any destination (for ingress). + // Given we're gonna allow all, we may as well break here and keep only this rule + protocolPorts = map[string][]numorstring.Port{"": nil} + break + } + + pStr := protocol.String() + protocolPorts[pStr] = append(protocolPorts[pStr], calicoPorts...) + } + + protocols := make([]string, 0, len(protocolPorts)) + for k := range protocolPorts { + protocols = append(protocols, k) + } + // Ensure deterministic output + sort.Strings(protocols) + + // Combine destinations with sources to generate rules. We generate one rule per protocol, + // with each rule containing all the allowed ports. + for _, protocolStr := range protocols { + calicoPorts := protocolPorts[protocolStr] + + var protocol *numorstring.Protocol + if protocolStr != "" { + p := numorstring.ProtocolFromString(protocolStr) + protocol = &p + } + for _, peer := range peers { selector, nsSelector, nets, notNets := c.k8sPeerToCalicoFields(peer, ns) if ingress { diff --git a/vendor/github.com/projectcalico/libcalico-go/lib/backend/model/block.go b/vendor/github.com/projectcalico/libcalico-go/lib/backend/model/block.go index a9de73326..10f55a603 100644 --- a/vendor/github.com/projectcalico/libcalico-go/lib/backend/model/block.go +++ b/vendor/github.com/projectcalico/libcalico-go/lib/backend/model/block.go @@ -120,7 +120,7 @@ func (b *AllocationBlock) IsDeleted() bool { func (b *AllocationBlock) Host() string { if b.Affinity != nil && strings.HasPrefix(*b.Affinity, "host:") { - return strings.TrimPrefix(*b.Affinity, "host:") + return strings.TrimLeft(*b.Affinity, "host:") } return "" } diff --git a/vendor/modules.txt b/vendor/modules.txt index d2a284a80..e6993045b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -471,7 +471,7 @@ github.com/pquerna/cachecontrol/cacheobject # github.com/projectcalico/kube-controllers v3.8.8+incompatible => github.com/projectcalico/kube-controllers v3.8.8+incompatible github.com/projectcalico/kube-controllers/pkg/cache github.com/projectcalico/kube-controllers/pkg/converter -# github.com/projectcalico/libcalico-go v1.7.2-0.20191104213956-8f81e1e344ce => github.com/projectcalico/libcalico-go v1.7.2-0.20191104213956-8f81e1e344ce +# github.com/projectcalico/libcalico-go v1.7.2-0.20191014160346-2382c6cdd056 => github.com/projectcalico/libcalico-go v1.7.2-0.20191014160346-2382c6cdd056 github.com/projectcalico/libcalico-go/lib/apis/v1 github.com/projectcalico/libcalico-go/lib/apis/v1/unversioned github.com/projectcalico/libcalico-go/lib/apis/v3 From d0c766c311e9258068274042766ff0372702830e Mon Sep 17 00:00:00 2001 From: "Roland.Ma" Date: Thu, 25 Feb 2021 06:32:44 +0000 Subject: [PATCH 11/21] fix: devops roles not been initialed Signed-off-by: Roland.Ma --- .../namespace/namespace_controller.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/pkg/controller/namespace/namespace_controller.go b/pkg/controller/namespace/namespace_controller.go index 3b88eacfd..7d213a8b0 100644 --- a/pkg/controller/namespace/namespace_controller.go +++ b/pkg/controller/namespace/namespace_controller.go @@ -20,6 +20,8 @@ import ( "bytes" "context" "fmt" + "reflect" + "github.com/go-logr/logr" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -37,7 +39,6 @@ import ( controllerutils "kubesphere.io/kubesphere/pkg/controller/utils/controller" "kubesphere.io/kubesphere/pkg/utils/k8sutil" "kubesphere.io/kubesphere/pkg/utils/sliceutil" - "reflect" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -129,19 +130,24 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { return ctrl.Result{}, nil } - // initialize subresource if created by kubesphere - if workspace := namespace.Labels[tenantv1alpha1.WorkspaceLabel]; workspace != "" { + // Bind to workspace if the namespace created by kubesphere + _, hasWorkspaceLabel := namespace.Labels[tenantv1alpha1.WorkspaceLabel] + if hasWorkspaceLabel { if err := r.bindWorkspace(rootCtx, logger, namespace); err != nil { return ctrl.Result{}, err } - if err := r.initRoles(rootCtx, logger, namespace); err != nil { - return ctrl.Result{}, err - } } else { if err := r.unbindWorkspace(rootCtx, logger, namespace); err != nil { return ctrl.Result{}, err } } + // Initialize roles for devops/project namespaces if created by kubesphere + _, hasDevOpsProjectLabel := namespace.Labels[constants.DevOpsProjectLabelKey] + if hasDevOpsProjectLabel || hasWorkspaceLabel { + if err := r.initRoles(rootCtx, logger, namespace); err != nil { + return ctrl.Result{}, err + } + } r.Recorder.Event(namespace, corev1.EventTypeNormal, controllerutils.SuccessSynced, controllerutils.MessageResourceSynced) return ctrl.Result{}, nil From 7929dd361e35067cff5273ade13057e2ecab73c2 Mon Sep 17 00:00:00 2001 From: hongming Date: Fri, 26 Feb 2021 16:13:25 +0800 Subject: [PATCH 12/21] Fix cannot modify UserSpec Signed-off-by: hongming --- pkg/kapis/iam/v1alpha2/handler.go | 2 -- pkg/models/iam/im/im.go | 5 +---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/pkg/kapis/iam/v1alpha2/handler.go b/pkg/kapis/iam/v1alpha2/handler.go index ac58cbec1..3aa9691e6 100644 --- a/pkg/kapis/iam/v1alpha2/handler.go +++ b/pkg/kapis/iam/v1alpha2/handler.go @@ -544,7 +544,6 @@ func (h *iamHandler) UpdateUser(request *restful.Request, response *restful.Resp var user iamv1alpha2.User err := request.ReadEntity(&user) - if err != nil { api.HandleBadRequest(response, request, err) return @@ -566,7 +565,6 @@ func (h *iamHandler) UpdateUser(request *restful.Request, response *restful.Resp } operator, ok := apirequest.UserFrom(request.Request.Context()) - if globalRole != "" && ok { err = h.updateGlobalRoleBinding(operator, updated, globalRole) if err != nil { diff --git a/pkg/models/iam/im/im.go b/pkg/models/iam/im/im.go index a9c1e128d..26fcf3997 100644 --- a/pkg/models/iam/im/im.go +++ b/pkg/models/iam/im/im.go @@ -64,12 +64,9 @@ func (im *imOperator) UpdateUser(new *iamv1alpha2.User) (*iamv1alpha2.User, erro klog.Error(err) return nil, err } - if old.Annotations == nil { - old.Annotations = make(map[string]string, 0) - } // keep encrypted password new.Spec.EncryptedPassword = old.Spec.EncryptedPassword - updated, err := im.ksClient.IamV1alpha2().Users().Update(context.Background(), old, metav1.UpdateOptions{}) + updated, err := im.ksClient.IamV1alpha2().Users().Update(context.Background(), new, metav1.UpdateOptions{}) if err != nil { klog.Error(err) return nil, err From 88eb5b8f3a4c6f12027dee563f1a0870e6c9413a Mon Sep 17 00:00:00 2001 From: yuswift Date: Sun, 28 Feb 2021 21:19:52 +0800 Subject: [PATCH 13/21] update roadmap for multi-cluster Signed-off-by: yuswift --- docs/roadmap.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index c077d4553..7a2f7be9b 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -105,6 +105,14 @@ KubeSphere Roadmap demonstrates a list of open source product development plans - [ ] Support for viewing resource consumption at the cluster, workspace, and application template levels [#3062](https://github.com/kubesphere/kubesphere/issues/3062) +### MultiCluster: + +- [x] Validate member cluster configuration if the member cluster config(e.g. jwtSecret) is not same as host cluster, which can reduce the complexity of joining clusters. [#3232](https://github.com/kubesphere/kubesphere/issues/3232) +- [x] Support configuring cluster controller resync period, some users need to update the cluster component more quickly. [#3213](https://github.com/kubesphere/kubesphere/issues/3213) +- [x] Lightweight member cluster installation, support running kubepshere without redis and ldap and so on. [#3056](https://github.com/kubesphere/kubesphere/issues/3056) +- [x] Refactor cluster controller(it should updates the cluster.staus field instead of the tower server when using proxy connection). [#3234](https://github.com/kubesphere/kubesphere/issues/3234) +- [ ] Support the tower server and agent running highly available(server with leader election option and running more than one agent). [#31](https://github.com/kubesphere/tower/issues/31) + ## **Upgrade:** - [x] Upgrade isito version from 1.4.8 => 1.6.10[#3326](https://github.com/kubesphere/kubesphere/issues/3236) @@ -119,14 +127,7 @@ KubeSphere Roadmap demonstrates a list of open source product development plans - [x] Using human-readable error message for pipeline cron text , [#2919](https://github.com/kubesphere/kubesphere/issues/2919) - [ ] Using human-readable error message for S2I, [#140](https://github.com/kubesphere/s2ioperator/issues/140) -### MultiCluster: - -- [x] Validate member cluster configuration if the member cluster config(e.g. jwtSecret) is not same as host cluster, which can reduce the complexity of joining clusters. [#3232](https://github.com/kubesphere/kubesphere/issues/3232) -- [ ] Support configuring cluster controller resync period, some users need to update the cluster component more quickly. [#3213](https://github.com/kubesphere/kubesphere/issues/3213) -- [x] Lightweight member cluster installation, support running kubepshere without redis and ldap and so on. [#3056](https://github.com/kubesphere/kubesphere/issues/3056) -- [ ] The cluster controller updates the cluster.staus field instead of the tower server when using proxy connection. [#3234](https://github.com/kubesphere/kubesphere/issues/3234) -- [ ] Support the tower server and agent running highly available(server with leader election option and running more than one agent). [#31](https://github.com/kubesphere/tower/issues/31) -- [ ] Support adding member clusters for an existing project(ns), wich can be helpful when there are new clusters joined. [#3246](https://github.com/kubesphere/kubesphere/issues/3246) +- [ ] https://github.com/kubesphere/tower/issues/31) ### Observability - [ ] Upgrade to Prometheus v2.25.0 From c28afabdd534867e3e6f04428329863478b4b27f Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 24 Feb 2021 20:57:11 +0800 Subject: [PATCH 14/21] application controller will only reconcile applications matched with given label selector Signed-off-by: Jeff --- cmd/controller-manager/app/options/options.go | 28 +- cmd/controller-manager/app/server.go | 18 +- go.mod | 26 +- go.sum | 10 + .../iam/v1alpha2/zz_generated.deepcopy.go | 5 + .../servicemesh/v1alpha2/strategy_types.go | 10 +- .../application/application_controller.go | 271 ++++++++++++++---- .../application_controller_test.go | 177 ++++++++---- .../application/application_suit_test.go | 118 +++++--- .../controller/application}/condition.go | 2 +- .../controller/application}/status.go | 2 +- vendor/kubesphere.io/client-go | 1 - vendor/modules.txt | 5 +- .../controllers/application_controller.go | 245 ---------------- 14 files changed, 495 insertions(+), 423 deletions(-) rename {vendor/sigs.k8s.io/application/controllers => pkg/controller/application}/condition.go (99%) rename {vendor/sigs.k8s.io/application/controllers => pkg/controller/application}/status.go (99%) delete mode 120000 vendor/kubesphere.io/client-go delete mode 100644 vendor/sigs.k8s.io/application/controllers/application_controller.go diff --git a/cmd/controller-manager/app/options/options.go b/cmd/controller-manager/app/options/options.go index 2d98ef8b4..698366695 100644 --- a/cmd/controller-manager/app/options/options.go +++ b/cmd/controller-manager/app/options/options.go @@ -18,6 +18,7 @@ package options import ( "flag" + "k8s.io/apimachinery/pkg/labels" "strings" "time" @@ -49,6 +50,15 @@ type KubeSphereControllerManagerOptions struct { LeaderElect bool LeaderElection *leaderelection.LeaderElectionConfig WebhookCertDir string + + // KubeSphere is using sigs.k8s.io/application as fundamental object to implement Application Management. + // There are other projects also built on sigs.k8s.io/application, when KubeSphere installed along side + // them, conflicts happen. So we leave an option to only reconcile applications matched with the given + // selector. Default will reconcile all applications. + // For example + // "kubesphere.io/creator=" means reconcile applications with this label key + // "!kubesphere.io/creator" means exclude applications with this key + ApplicationSelector string } func NewKubeSphereControllerManagerOptions() *KubeSphereControllerManagerOptions { @@ -67,8 +77,9 @@ func NewKubeSphereControllerManagerOptions() *KubeSphereControllerManagerOptions RenewDeadline: 15 * time.Second, RetryPeriod: 5 * time.Second, }, - LeaderElect: false, - WebhookCertDir: "", + LeaderElect: false, + WebhookCertDir: "", + ApplicationSelector: "", } return s @@ -99,6 +110,11 @@ func (s *KubeSphereControllerManagerOptions) Flags() cliflag.NamedFlagSets { "if not set, webhook server would look up the server key and certificate in"+ "{TempDir}/k8s-webhook-server/serving-certs") + gfs := fss.FlagSet("generic") + gfs.StringVar(&s.ApplicationSelector, "application-selector", s.ApplicationSelector, ""+ + "Only reconcile application(sigs.k8s.io/application) objects match given selector, this could avoid conflicts with "+ + "other projects built on top of sig-application. Default behavior is to reconcile all of application objects.") + kfs := fss.FlagSet("klog") local := flag.NewFlagSet("klog", flag.ExitOnError) klog.InitFlags(local) @@ -118,6 +134,14 @@ func (s *KubeSphereControllerManagerOptions) Validate() []error { errs = append(errs, s.OpenPitrixOptions.Validate()...) errs = append(errs, s.NetworkOptions.Validate()...) errs = append(errs, s.LdapOptions.Validate()...) + + if len(s.ApplicationSelector) != 0 { + _, err := labels.Parse(s.ApplicationSelector) + if err != nil { + errs = append(errs, err) + } + } + return errs } diff --git a/cmd/controller-manager/app/server.go b/cmd/controller-manager/app/server.go index d0fc7399b..e93c87065 100644 --- a/cmd/controller-manager/app/server.go +++ b/cmd/controller-manager/app/server.go @@ -18,6 +18,8 @@ package app import ( "fmt" + "k8s.io/apimachinery/pkg/labels" + "kubesphere.io/kubesphere/pkg/controller/application" "os" "github.com/spf13/cobra" @@ -28,7 +30,6 @@ import ( "kubesphere.io/kubesphere/cmd/controller-manager/app/options" "kubesphere.io/kubesphere/pkg/apis" controllerconfig "kubesphere.io/kubesphere/pkg/apiserver/config" - appcontroller "kubesphere.io/kubesphere/pkg/controller/application" "kubesphere.io/kubesphere/pkg/controller/namespace" "kubesphere.io/kubesphere/pkg/controller/network/webhooks" "kubesphere.io/kubesphere/pkg/controller/serviceaccount" @@ -46,7 +47,6 @@ import ( "kubesphere.io/kubesphere/pkg/simple/client/s3" "kubesphere.io/kubesphere/pkg/utils/metrics" "kubesphere.io/kubesphere/pkg/utils/term" - application "sigs.k8s.io/application/controllers" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/runtime/signals" @@ -219,16 +219,12 @@ func run(s *options.KubeSphereControllerManagerOptions, stopCh <-chan struct{}) klog.Fatal("Unable to create namespace controller") } - err = appcontroller.Add(mgr) - if err != nil { - klog.Fatal("Unable to create ks application controller") - } - + selector, _ := labels.Parse(s.ApplicationSelector) applicationReconciler := &application.ApplicationReconciler{ - Scheme: mgr.GetScheme(), - Client: mgr.GetClient(), - Mapper: mgr.GetRESTMapper(), - Log: klogr.New(), + Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Mapper: mgr.GetRESTMapper(), + ApplicationSelector: selector, } if err = applicationReconciler.SetupWithManager(mgr); err != nil { klog.Fatal("Unable to create application controller") diff --git a/go.mod b/go.mod index 942e86631..5c3196d49 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/gorilla/websocket v1.4.1 github.com/json-iterator/go v1.1.10 github.com/kelseyhightower/envconfig v1.4.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/kubernetes-csi/external-snapshotter/client/v3 v3.0.0 github.com/kubesphere/sonargo v0.0.2 github.com/mitchellh/mapstructure v1.2.2 @@ -69,12 +70,14 @@ require ( github.com/spf13/viper v1.4.0 github.com/stretchr/testify v1.6.1 github.com/xanzy/ssh-agent v0.2.1 // indirect - golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de + golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 golang.org/x/net v0.0.0-20200707034311-ab3426394381 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d + golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect google.golang.org/grpc v1.30.0 gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect gopkg.in/cas.v2 v2.2.0 + gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/square/go-jose.v2 v2.4.0 gopkg.in/src-d/go-billy.v4 v4.3.0 // indirect gopkg.in/src-d/go-git.v4 v4.11.0 @@ -189,6 +192,7 @@ replace ( github.com/brancz/kube-rbac-proxy => github.com/brancz/kube-rbac-proxy v0.5.0 github.com/bshuster-repo/logrus-logstash-hook => github.com/bshuster-repo/logrus-logstash-hook v0.4.1 github.com/bugsnag/bugsnag-go => github.com/bugsnag/bugsnag-go v1.5.0 + github.com/bugsnag/osext => github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b github.com/bugsnag/panicwrap => github.com/bugsnag/panicwrap v1.2.0 github.com/c-bata/go-prompt => github.com/c-bata/go-prompt v0.2.2 github.com/campoy/embedmd => github.com/campoy/embedmd v1.0.0 @@ -207,6 +211,7 @@ replace ( github.com/circonus-labs/circonus-gometrics => github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible github.com/circonus-labs/circonusllhist => github.com/circonus-labs/circonusllhist v0.1.3 github.com/clbanning/x2j => github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec + github.com/cloudflare/cfssl => github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 github.com/cockroachdb/apd => github.com/cockroachdb/apd v1.1.0 github.com/cockroachdb/cockroach-go => github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c github.com/cockroachdb/datadriven => github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa @@ -214,6 +219,9 @@ replace ( github.com/container-storage-interface/spec => github.com/container-storage-interface/spec v1.2.0 github.com/containerd/containerd => github.com/containerd/containerd v1.3.0 github.com/containerd/continuity => github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 + github.com/containerd/fifo => github.com/containerd/fifo v0.0.0-20210129194248-f8e8fdba47ef + github.com/containerd/ttrpc => github.com/containerd/ttrpc v1.0.2 + github.com/containerd/typeurl => github.com/containerd/typeurl v1.0.1 github.com/containernetworking/cni => github.com/containernetworking/cni v0.8.0 github.com/coreos/bbolt => github.com/coreos/bbolt v1.3.3 github.com/coreos/etcd => github.com/coreos/etcd v3.3.17+incompatible @@ -247,16 +255,21 @@ replace ( github.com/dhui/dktest => github.com/dhui/dktest v0.3.0 github.com/disintegration/imaging => github.com/disintegration/imaging v1.6.1 github.com/docker/cli => github.com/docker/cli v0.0.0-20190506213505-d88565df0c2d + github.com/docker/compose-on-kubernetes => github.com/docker/compose-on-kubernetes v0.4.24 github.com/docker/distribution => github.com/docker/distribution v2.7.1+incompatible github.com/docker/docker => github.com/docker/engine v1.4.2-0.20190822205725-ed20165a37b4 github.com/docker/docker-credential-helpers => github.com/docker/docker-credential-helpers v0.6.1 + github.com/docker/go => github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c github.com/docker/go-connections => github.com/docker/go-connections v0.4.0 + github.com/docker/go-events => github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c github.com/docker/go-metrics => github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82 github.com/docker/go-units => github.com/docker/go-units v0.4.0 github.com/docker/libtrust => github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 github.com/docker/spdystream => github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c + github.com/docker/swarmkit => github.com/docker/swarmkit v1.12.0 github.com/docopt/docopt-go => github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 github.com/dustin/go-humanize => github.com/dustin/go-humanize v1.0.0 + github.com/dvsekhvalnov/jose2go => github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae github.com/eapache/go-resiliency => github.com/eapache/go-resiliency v1.1.0 github.com/eapache/go-xerial-snappy => github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 github.com/eapache/queue => github.com/eapache/queue v1.1.0 @@ -360,6 +373,7 @@ replace ( github.com/gomodule/redigo => github.com/gomodule/redigo v2.0.0+incompatible github.com/google/addlicense => github.com/google/addlicense v0.0.0-20200906110928-a0294312aa76 github.com/google/btree => github.com/google/btree v1.0.0 + github.com/google/certificate-transparency-go => github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93 github.com/google/flatbuffers => github.com/google/flatbuffers v1.11.0 github.com/google/go-cmp => github.com/google/go-cmp v0.4.0 github.com/google/go-github => github.com/google/go-github v17.0.0+incompatible @@ -369,6 +383,7 @@ replace ( github.com/google/martian => github.com/google/martian v2.1.0+incompatible github.com/google/pprof => github.com/google/pprof v0.0.0-20200417002340-c6e0a841f49a github.com/google/renameio => github.com/google/renameio v0.1.0 + github.com/google/shlex => github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid => github.com/google/uuid v1.1.1 github.com/googleapis/gax-go => github.com/googleapis/gax-go v2.0.2+incompatible github.com/googleapis/gax-go/v2 => github.com/googleapis/gax-go/v2 v2.0.5 @@ -437,6 +452,7 @@ replace ( github.com/jstemmer/go-junit-report => github.com/jstemmer/go-junit-report v0.9.1 github.com/jsternberg/zap-logfmt => github.com/jsternberg/zap-logfmt v1.0.0 github.com/jtolds/gls => github.com/jtolds/gls v4.20.0+incompatible + github.com/juju/loggo => github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 github.com/julienschmidt/httprouter => github.com/julienschmidt/httprouter v1.3.0 github.com/jung-kurt/gofpdf => github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5 github.com/jwilder/encoding => github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef @@ -488,6 +504,7 @@ replace ( github.com/mdlayher/wifi => github.com/mdlayher/wifi v0.0.0-20190303161829-b1436901ddee github.com/mgutz/ansi => github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b github.com/miekg/dns => github.com/miekg/dns v1.1.29 + github.com/miekg/pkcs11 => github.com/miekg/pkcs11 v1.0.2 github.com/minio/md5-simd => github.com/minio/md5-simd v1.1.0 github.com/minio/minio-go/v7 => github.com/minio/minio-go/v7 v7.0.2 github.com/minio/sha256-simd => github.com/minio/sha256-simd v0.1.1 @@ -517,6 +534,7 @@ replace ( github.com/nats-io/nkeys => github.com/nats-io/nkeys v0.1.3 github.com/nats-io/nuid => github.com/nats-io/nuid v1.0.1 github.com/ncw/swift => github.com/ncw/swift v1.0.50 + github.com/niemeyer/pretty => github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e github.com/nxadm/tail => github.com/nxadm/tail v1.4.4 github.com/oklog/oklog => github.com/oklog/oklog v0.3.2 github.com/oklog/run => github.com/oklog/run v1.1.0 @@ -529,6 +547,7 @@ replace ( github.com/opencontainers/go-digest => github.com/opencontainers/go-digest v1.0.0-rc1 github.com/opencontainers/image-spec => github.com/opencontainers/image-spec v1.0.1 github.com/opencontainers/runc => github.com/opencontainers/runc v0.1.1 + github.com/opencontainers/runtime-spec => github.com/opencontainers/runtime-spec v1.0.2 github.com/opentracing-contrib/go-grpc => github.com/opentracing-contrib/go-grpc v0.0.0-20180928155321-4b5a12d3ff02 github.com/opentracing-contrib/go-observer => github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 github.com/opentracing-contrib/go-stdlib => github.com/opentracing-contrib/go-stdlib v0.0.0-20190519235532-cf7a6c988dc9 @@ -614,7 +633,9 @@ replace ( github.com/streadway/handy => github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a github.com/stretchr/objx => github.com/stretchr/objx v0.2.0 github.com/stretchr/testify => github.com/stretchr/testify v1.4.0 + github.com/syndtr/gocapability => github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 github.com/thanos-io/thanos => github.com/thanos-io/thanos v0.13.1-0.20200910143741-e0b7f7b32e9c + github.com/theupdateframework/notary => github.com/theupdateframework/notary v0.7.0 github.com/tidwall/pretty => github.com/tidwall/pretty v1.0.0 github.com/tinylib/msgp => github.com/tinylib/msgp v1.1.0 github.com/tmc/grpc-websocket-proxy => github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 @@ -668,6 +689,7 @@ replace ( golang.org/x/oauth2 => golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a golang.org/x/sync => golang.org/x/sync v0.0.0-20190423024810-112230192c58 golang.org/x/sys => golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e + golang.org/x/term => golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 golang.org/x/text => golang.org/x/text v0.3.0 golang.org/x/time => golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 golang.org/x/tools => golang.org/x/tools v0.0.0-20190710153321-831012c29e42 @@ -686,6 +708,7 @@ replace ( gopkg.in/alexcesaro/quotedprintable.v3 => gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc gopkg.in/asn1-ber.v1 => gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d gopkg.in/cas.v2 => gopkg.in/cas.v2 v2.2.0 + gopkg.in/cenkalti/backoff.v2 => gopkg.in/cenkalti/backoff.v2 v2.2.1 gopkg.in/check.v1 => gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 gopkg.in/cheggaaa/pb.v1 => gopkg.in/cheggaaa/pb.v1 v1.0.25 gopkg.in/errgo.v2 => gopkg.in/errgo.v2 v2.1.0 @@ -701,6 +724,7 @@ replace ( gopkg.in/ini.v1 => gopkg.in/ini.v1 v1.57.0 gopkg.in/mail.v2 => gopkg.in/mail.v2 v2.3.1 gopkg.in/natefinch/lumberjack.v2 => gopkg.in/natefinch/lumberjack.v2 v2.0.0 + gopkg.in/rethinkdb/rethinkdb-go.v6 => gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 gopkg.in/square/go-jose.v1 => gopkg.in/square/go-jose.v1 v1.1.2 gopkg.in/square/go-jose.v2 => gopkg.in/square/go-jose.v2 v2.4.0 gopkg.in/src-d/go-billy.v4 => gopkg.in/src-d/go-billy.v4 v4.3.0 diff --git a/go.sum b/go.sum index 1cd294de9..fa0829362 100644 --- a/go.sum +++ b/go.sum @@ -130,11 +130,14 @@ github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/container-storage-interface/spec v1.2.0 h1:bD9KIVgaVKKkQ/UbVUY9kCaH/CJbhNxe0eeB4JeJV2s= github.com/container-storage-interface/spec v1.2.0/go.mod h1:6URME8mwIBbpVyZV93Ce5St17xBiQJQY67NDsuohiy4= +github.com/containerd/containerd v1.3.0 h1:xjvXQWABwS2uiv3TWgQt5Uth60Gu86LTGZXMJkjc7rY= github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 h1:4BX8f882bXEDKfWIf0wa8HRvpnBoPszJJXL+TVbBw4M= github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containernetworking/cni v0.8.0 h1:BT9lpgGoH4jw3lFC7Odz2prU5ruiYKcgAjMCbgybcKI= github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.17+incompatible h1:f/Z3EoDSx1yjaIjLQGo1diYUlQYSBrrAQ5vP8NjwXwo= github.com/coreos/etcd v3.3.17+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-oidc v2.1.0+incompatible h1:sdJrfw8akMnCuUlaZU3tE/uYXFgfqom8DBE9so9EBsM= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= @@ -172,14 +175,17 @@ github.com/dgryski/go-bitstream v0.0.0-20180413035011-3522498ce2c8/go.mod h1:VMa github.com/dgryski/go-sip13 v0.0.0-20190329191031-25c5027a8c7b/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dhui/dktest v0.3.0/go.mod h1:cyzIUfGsBEbZ6BT7tnXqAShHSXCZhSNmFl70sZ7c1yc= github.com/disintegration/imaging v1.6.1/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ= +github.com/docker/cli v0.0.0-20190506213505-d88565df0c2d h1:qdD+BtyCE1XXpDyhvn0yZVcZOLILdj9Cw4pKu0kQbPQ= github.com/docker/cli v0.0.0-20190506213505-d88565df0c2d/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker-credential-helpers v0.6.1 h1:Dq4iIfcM7cNtddhLVWe9h4QDjsi4OER3Z8voPu/I52g= github.com/docker/docker-credential-helpers v0.6.1/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= github.com/docker/engine v1.4.2-0.20190822205725-ed20165a37b4 h1:+VAGRKyn9Ca+ckzV/PJsaRO7UXO9KQjFmSffcSDrWdE= github.com/docker/engine v1.4.2-0.20190822205725-ed20165a37b4/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82 h1:X0fj836zx99zFu83v/M79DuBn84IL/Syx1SY6Y5ZEMA= github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= @@ -311,6 +317,7 @@ github.com/gocraft/dbr v0.0.0-20180507214907-a0fd650918f6/go.mod h1:K/9g3pPouf13 github.com/godbus/dbus v0.0.0-20190402143921-271e53dc4968/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/googleapis v1.1.0 h1:kFkMAZBNAn4j7K0GiZr8cRYzejq68VbheufiV3YuyFI= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= @@ -386,6 +393,7 @@ github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR3 github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= @@ -554,6 +562,7 @@ github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2i github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y= github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opentracing-contrib/go-grpc v0.0.0-20180928155321-4b5a12d3ff02/go.mod h1:JNdpVEzCpXBgIiv4ds+TzhN1hrtxq6ClLrTlT9OQRSc= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= @@ -890,4 +899,5 @@ sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnM sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= +vbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc h1:MksmcCZQWAQJCTA5T0jgI/0sJ51AVm4Z41MrmfczEoc= vbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= diff --git a/pkg/apis/iam/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/iam/v1alpha2/zz_generated.deepcopy.go index 2920eccd3..53810ab25 100644 --- a/pkg/apis/iam/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/iam/v1alpha2/zz_generated.deepcopy.go @@ -770,6 +770,11 @@ func (in *UserSpec) DeepCopy() *UserSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UserStatus) DeepCopyInto(out *UserStatus) { *out = *in + if in.State != nil { + in, out := &in.State, &out.State + *out = new(UserState) + **out = **in + } if in.LastTransitionTime != nil { in, out := &in.LastTransitionTime, &out.LastTransitionTime *out = (*in).DeepCopy() diff --git a/pkg/apis/servicemesh/v1alpha2/strategy_types.go b/pkg/apis/servicemesh/v1alpha2/strategy_types.go index c86b459d9..bf5bd227e 100644 --- a/pkg/apis/servicemesh/v1alpha2/strategy_types.go +++ b/pkg/apis/servicemesh/v1alpha2/strategy_types.go @@ -31,17 +31,17 @@ const ( ResourcePluralStrategy = "strategies" ) -type StrategyType string +type strategyType string const ( // Canary strategy type - CanaryType StrategyType = "Canary" + CanaryType strategyType = "Canary" // BlueGreen strategy type - BlueGreenType StrategyType = "BlueGreen" + BlueGreenType strategyType = "BlueGreen" // Mirror strategy type - Mirror StrategyType = "Mirror" + Mirror strategyType = "Mirror" ) type StrategyPolicy string @@ -60,7 +60,7 @@ const ( // StrategySpec defines the desired state of Strategy type StrategySpec struct { // Strategy type - Type StrategyType `json:"type,omitempty"` + Type strategyType `json:"type,omitempty"` // Principal version, the one as reference version // label version value diff --git a/pkg/controller/application/application_controller.go b/pkg/controller/application/application_controller.go index ab8658132..9ef9bd8b7 100644 --- a/pkg/controller/application/application_controller.go +++ b/pkg/controller/application/application_controller.go @@ -18,45 +18,239 @@ package application import ( "context" + "fmt" v1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" v1beta12 "k8s.io/api/networking/v1beta1" - "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/tools/record" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/client-go/util/retry" "k8s.io/klog" servicemeshv1alpha2 "kubesphere.io/kubesphere/pkg/apis/servicemesh/v1alpha2" "kubesphere.io/kubesphere/pkg/controller/utils/servicemesh" - "sigs.k8s.io/application/api/v1beta1" + appv1beta1 "sigs.k8s.io/application/api/v1beta1" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" - "time" ) -// Add creates a new Application Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller -// and Start it when the Manager is Started. -func Add(mgr manager.Manager) error { - return add(mgr, newReconciler(mgr)) +// ApplicationReconciler reconciles a Application object +type ApplicationReconciler struct { + client.Client + Mapper meta.RESTMapper + Scheme *runtime.Scheme + ApplicationSelector labels.Selector // } -// newReconciler returns a new reconcile.Reconciler -func newReconciler(mgr manager.Manager) reconcile.Reconciler { - return &ReconcileApplication{Client: mgr.GetClient(), scheme: mgr.GetScheme(), - recorder: mgr.GetEventRecorderFor("application-controller")} +func (r *ApplicationReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + var app appv1beta1.Application + err := r.Get(context.Background(), req.NamespacedName, &app) + if err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + // If label selector were given, only reconcile matched applications + // match annotations and labels + if !r.ApplicationSelector.Empty() { + if !r.ApplicationSelector.Matches(labels.Set(app.Labels)) && + !r.ApplicationSelector.Matches(labels.Set(app.Annotations)) { + return ctrl.Result{}, err + } + } + + // Application is in the process of being deleted, so no need to do anything. + if app.DeletionTimestamp != nil { + return ctrl.Result{}, nil + } + + resources, errs := r.updateComponents(context.Background(), &app) + newApplicationStatus := r.getNewApplicationStatus(context.Background(), &app, resources, &errs) + + newApplicationStatus.ObservedGeneration = app.Generation + if equality.Semantic.DeepEqual(newApplicationStatus, &app.Status) { + return ctrl.Result{}, nil + } + + err = r.updateApplicationStatus(context.Background(), req.NamespacedName, newApplicationStatus) + return ctrl.Result{}, err } -// add adds a new Controller to mgr with r as the reconcile.Reconciler -func add(mgr manager.Manager, r reconcile.Reconciler) error { - // Create a new controller - c, err := controller.New("application-controller", mgr, controller.Options{Reconciler: r}) +func (r *ApplicationReconciler) updateComponents(ctx context.Context, app *appv1beta1.Application) ([]*unstructured.Unstructured, []error) { + var errs []error + resources := r.fetchComponentListResources(ctx, app.Spec.ComponentGroupKinds, app.Spec.Selector, app.Namespace, &errs) + + if app.Spec.AddOwnerRef { + ownerRef := metav1.NewControllerRef(app, appv1beta1.GroupVersion.WithKind("Application")) + *ownerRef.Controller = false + if err := r.setOwnerRefForResources(ctx, *ownerRef, resources); err != nil { + errs = append(errs, err) + } + } + return resources, errs +} + +func (r *ApplicationReconciler) getNewApplicationStatus(ctx context.Context, app *appv1beta1.Application, resources []*unstructured.Unstructured, errList *[]error) *appv1beta1.ApplicationStatus { + objectStatuses := r.objectStatuses(ctx, resources, errList) + errs := utilerrors.NewAggregate(*errList) + + aggReady, countReady := aggregateReady(objectStatuses) + + newApplicationStatus := app.Status.DeepCopy() + newApplicationStatus.ComponentList = appv1beta1.ComponentList{ + Objects: objectStatuses, + } + newApplicationStatus.ComponentsReady = fmt.Sprintf("%d/%d", countReady, len(objectStatuses)) + if errs != nil { + setReadyUnknownCondition(newApplicationStatus, "ComponentsReadyUnknown", "failed to aggregate all components' statuses, check the Error condition for details") + } else if aggReady { + setReadyCondition(newApplicationStatus, "ComponentsReady", "all components ready") + } else { + setNotReadyCondition(newApplicationStatus, "ComponentsNotReady", fmt.Sprintf("%d components not ready", len(objectStatuses)-countReady)) + } + + if errs != nil { + setErrorCondition(newApplicationStatus, "ErrorSeen", errs.Error()) + } else { + clearErrorCondition(newApplicationStatus) + } + + return newApplicationStatus +} + +func (r *ApplicationReconciler) fetchComponentListResources(ctx context.Context, groupKinds []metav1.GroupKind, selector *metav1.LabelSelector, namespace string, errs *[]error) []*unstructured.Unstructured { + var resources []*unstructured.Unstructured + + if selector == nil { + klog.V(2).Info("No selector is specified") + return resources + } + + for _, gk := range groupKinds { + mapping, err := r.Mapper.RESTMapping(schema.GroupKind{ + Group: appv1beta1.StripVersion(gk.Group), + Kind: gk.Kind, + }) + if err != nil { + klog.V(2).Info("NoMappingForGK", "gk", gk.String()) + continue + } + + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(mapping.GroupVersionKind) + if err = r.Client.List(ctx, list, client.InNamespace(namespace), client.MatchingLabels(selector.MatchLabels)); err != nil { + klog.Error(err, "unable to list resources for GVK", "gvk", mapping.GroupVersionKind) + *errs = append(*errs, err) + continue + } + + for _, u := range list.Items { + resource := u + resources = append(resources, &resource) + } + } + return resources +} + +func (r *ApplicationReconciler) setOwnerRefForResources(ctx context.Context, ownerRef metav1.OwnerReference, resources []*unstructured.Unstructured) error { + for _, resource := range resources { + ownerRefs := resource.GetOwnerReferences() + ownerRefFound := false + for i, refs := range ownerRefs { + if ownerRef.Kind == refs.Kind && + ownerRef.APIVersion == refs.APIVersion && + ownerRef.Name == refs.Name { + ownerRefFound = true + if ownerRef.UID != refs.UID { + ownerRefs[i] = ownerRef + } + } + } + + if !ownerRefFound { + ownerRefs = append(ownerRefs, ownerRef) + } + resource.SetOwnerReferences(ownerRefs) + err := r.Client.Update(ctx, resource) + if err != nil { + // We log this error, but we continue and try to set the ownerRefs on the other resources. + klog.Error(err, "ErrorSettingOwnerRef", "gvk", resource.GroupVersionKind().String(), + "namespace", resource.GetNamespace(), "name", resource.GetName()) + } + } + return nil +} + +func (r *ApplicationReconciler) objectStatuses(ctx context.Context, resources []*unstructured.Unstructured, errs *[]error) []appv1beta1.ObjectStatus { + var objectStatuses []appv1beta1.ObjectStatus + for _, resource := range resources { + os := appv1beta1.ObjectStatus{ + Group: resource.GroupVersionKind().Group, + Kind: resource.GetKind(), + Name: resource.GetName(), + Link: resource.GetSelfLink(), + } + s, err := status(resource) + if err != nil { + klog.Error(err, "unable to compute status for resource", "gvk", resource.GroupVersionKind().String(), + "namespace", resource.GetNamespace(), "name", resource.GetName()) + *errs = append(*errs, err) + } + os.Status = s + objectStatuses = append(objectStatuses, os) + } + return objectStatuses +} + +func aggregateReady(objectStatuses []appv1beta1.ObjectStatus) (bool, int) { + countReady := 0 + for _, os := range objectStatuses { + if os.Status == StatusReady { + countReady++ + } + } + if countReady == len(objectStatuses) { + return true, countReady + } + return false, countReady +} + +func (r *ApplicationReconciler) updateApplicationStatus(ctx context.Context, nn types.NamespacedName, status *appv1beta1.ApplicationStatus) error { + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + original := &appv1beta1.Application{} + if err := r.Get(ctx, nn, original); err != nil { + return err + } + original.Status = *status + if err := r.Client.Status().Update(ctx, original); err != nil { + return err + } + return nil + }); err != nil { + return fmt.Errorf("failed to update status of Application %s/%s: %v", nn.Namespace, nn.Name, err) + } + return nil +} + +func (r *ApplicationReconciler) SetupWithManager(mgr ctrl.Manager) error { + c, err := ctrl.NewControllerManagedBy(mgr). + Named("application-controller"). + For(&appv1beta1.Application{}).Build(r) + if err != nil { return err } @@ -98,46 +292,7 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { return nil } -var _ reconcile.Reconciler = &ReconcileApplication{} - -// ReconcileApplication reconciles a Workspace object -type ReconcileApplication struct { - client.Client - scheme *runtime.Scheme - recorder record.EventRecorder -} - -// +kubebuilder:rbac:groups=app.k8s.io,resources=applications,verbs=get;list;watch;create;update;patch;delete -func (r *ReconcileApplication) Reconcile(request reconcile.Request) (reconcile.Result, error) { - // Fetch the Application instance - ctx := context.Background() - app := &v1beta1.Application{} - err := r.Get(ctx, request.NamespacedName, app) - if err != nil { - if errors.IsNotFound(err) { - klog.Errorf("application %s not found in namespace %s", request.Name, request.Namespace) - return reconcile.Result{}, nil - } - return reconcile.Result{}, err - } - - // add specified annotation for app when triggered by sub-resources, - // so the application in sigs.k8s.io can reconcile to update status - annotations := app.GetObjectMeta().GetAnnotations() - if annotations == nil { - annotations = make(map[string]string) - } - annotations["kubesphere.io/last-updated"] = time.Now().String() - app.SetAnnotations(annotations) - err = r.Update(ctx, app) - if err != nil { - if errors.IsNotFound(err) { - klog.V(4).Infof("application %s has been deleted during update in namespace %s", request.Name, request.Namespace) - return reconcile.Result{}, nil - } - } - return reconcile.Result{}, nil -} +var _ reconcile.Reconciler = &ApplicationReconciler{} func isApp(obs ...metav1.Object) bool { for _, o := range obs { diff --git a/pkg/controller/application/application_controller_test.go b/pkg/controller/application/application_controller_test.go index 723fc2e8c..e81d2755a 100644 --- a/pkg/controller/application/application_controller_test.go +++ b/pkg/controller/application/application_controller_test.go @@ -19,6 +19,10 @@ package application import ( "context" "fmt" + "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/controller-runtime/pkg/client" + "time" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" v1 "k8s.io/api/apps/v1" @@ -27,71 +31,131 @@ import ( "k8s.io/apimachinery/pkg/types" "kubesphere.io/kubesphere/pkg/controller/utils/servicemesh" "sigs.k8s.io/application/api/v1beta1" - "time" +) + +const ( + applicationName = "bookinfo" + serviceName = "productpage" + timeout = time.Second * 30 + interval = time.Second * 2 ) var replicas = int32(2) -var _ = Describe("Application", func() { - - const timeout = time.Second * 30 - const interval = time.Second * 1 +var _ = Context("Inside of a new namespace", func() { ctx := context.TODO() + ns := SetupTest(ctx) - service := newService("productpage") - app := newAppliation(service) - deployments := []*v1.Deployment{newDeployments(service, "v1")} - - BeforeEach(func() { - - // Create application service and deployment - Expect(k8sClient.Create(ctx, app)).Should(Succeed()) - Expect(k8sClient.Create(ctx, service)).Should(Succeed()) - for i := range deployments { - deployment := deployments[i] - Expect(k8sClient.Create(ctx, deployment)).Should(Succeed()) + Describe("Application", func() { + applicationLabels := map[string]string{ + "app.kubernetes.io/name": "bookinfo", + "app.kubernetes.io/version": "1", } - }) - // Add Tests for OpenAPI validation (or additonal CRD features) specified in - // your API definition. - // Avoid adding tests for vanilla CRUD operations because they would - // test Kubernetes API server, which isn't the goal here. - Context("Application Controller", func() { - It("Should create successfully", func() { + BeforeEach(func() { + By("create deployment,service,application objects") + service := newService(serviceName, ns.Name, applicationLabels) + deployments := []*v1.Deployment{newDeployments(serviceName, ns.Name, applicationLabels, "v1")} + app := newApplication(applicationName, ns.Name, applicationLabels) - By("Reconcile Application successfully") - // application should have "kubesphere.io/last-updated" annotation - Eventually(func() bool { - app := &v1beta1.Application{} - _ = k8sClient.Get(ctx, types.NamespacedName{Name: service.Labels[servicemesh.ApplicationNameLabel], Namespace: metav1.NamespaceDefault}, app) - time, ok := app.Annotations["kubesphere.io/last-updated"] - return len(time) > 0 && ok - }, timeout, interval).Should(BeTrue()) + Expect(k8sClient.Create(ctx, service.DeepCopy())).Should(Succeed()) + for i := range deployments { + deployment := deployments[i] + Expect(k8sClient.Create(ctx, deployment.DeepCopy())).Should(Succeed()) + } + Expect(k8sClient.Create(ctx, app)).Should(Succeed()) + }) + + Context("Application Controller", func() { + It("Should not reconcile application", func() { + By("update application labels") + application := &v1beta1.Application{} + + err := k8sClient.Get(ctx, types.NamespacedName{Name: applicationName, Namespace: ns.Name}, application) + Expect(err).Should(Succeed()) + + updateApplication := func(object interface{}) { + newApp := object.(*v1beta1.Application) + newApp.Labels["kubesphere.io/creator"] = "" + } + + updated, err := updateWithRetries(k8sClient, ctx, application.Namespace, applicationName, updateApplication, 1 * time.Second, 5 * time.Second) + Expect(updated).Should(BeTrue()) + + Eventually(func() bool { + + err = k8sClient.Get(ctx, types.NamespacedName{Name: applicationName, Namespace: ns.Name}, application) + + // application status field should not be populated with selected deployments and services + return len(application.Status.ComponentList.Objects) == 0 + }, timeout, interval).Should(BeTrue()) + + }) + + It("Should reconcile application successfully", func() { + + By("check if application status been updated by controller") + application := &v1beta1.Application{} + + Eventually(func() bool { + err := k8sClient.Get(ctx, types.NamespacedName{Name: applicationName, Namespace: ns.Name}, application) + Expect(err).Should(Succeed()) + + // application status field should be populated by controller + return len(application.Status.ComponentList.Objects) > 0 + }, timeout, interval).Should(BeTrue()) + + }) }) }) }) -func newDeployments(service *corev1.Service, version string) *v1.Deployment { - lbs := service.Labels - lbs["version"] = version +type UpdateObjectFunc func(obj interface{}) + +func updateWithRetries(client client.Client, ctx context.Context, namespace, name string, updateFunc UpdateObjectFunc, interval, timeout time.Duration)(bool, error) { + var updateErr error + + pollErr := wait.PollImmediate(interval, timeout, func() (done bool, err error) { + app := &v1beta1.Application{} + if err = client.Get(ctx, types.NamespacedName{Namespace: namespace, Name: name}, app); err != nil { + return false, err + } + + updateFunc(app) + if err = client.Update(ctx, app); err == nil { + return true, nil + } + + updateErr = err + return false, nil + }) + + if pollErr == wait.ErrWaitTimeout { + pollErr = fmt.Errorf("couldn't apply the provided update to object %q: %v", name, updateErr) + return false, pollErr + } + return true, nil +} + +func newDeployments(deploymentName, namespace string, labels map[string]string, version string) *v1.Deployment { + labels["app"] = deploymentName + labels["version"] = version deployment := &v1.Deployment{ ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s-%s", service.Name, version), - Namespace: metav1.NamespaceDefault, - Labels: lbs, + Name: fmt.Sprintf("%s-%s", deploymentName, version), + Namespace: namespace, + Labels: labels, Annotations: map[string]string{servicemesh.ServiceMeshEnabledAnnotation: "true"}, }, Spec: v1.DeploymentSpec{ Replicas: &replicas, Selector: &metav1.LabelSelector{ - MatchLabels: lbs, + MatchLabels: labels, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: lbs, - Annotations: service.Annotations, + Labels: labels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ @@ -130,16 +194,14 @@ func newDeployments(service *corev1.Service, version string) *v1.Deployment { return deployment } -func newService(name string) *corev1.Service { +func newService(serviceName, namesapce string, labels map[string]string) *corev1.Service { + labels["app"] = serviceName + svc := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: metav1.NamespaceDefault, - Labels: map[string]string{ - "app.kubernetes.io/name": "bookinfo", - "app.kubernetes.io/version": "1", - "app": name, - }, + Name: serviceName, + Namespace: namesapce, + Labels: labels, Annotations: map[string]string{ "servicemesh.kubesphere.io/enabled": "true", }, @@ -162,11 +224,7 @@ func newService(name string) *corev1.Service { Protocol: corev1.ProtocolTCP, }, }, - Selector: map[string]string{ - "app.kubernetes.io/name": "bookinfo", - "app.kubernetes.io/version": "1", - "app": "foo", - }, + Selector: labels, Type: corev1.ServiceTypeClusterIP, }, Status: corev1.ServiceStatus{}, @@ -174,12 +232,12 @@ func newService(name string) *corev1.Service { return svc } -func newAppliation(service *corev1.Service) *v1beta1.Application { +func newApplication(applicationName, namespace string, labels map[string]string) *v1beta1.Application { app := &v1beta1.Application{ ObjectMeta: metav1.ObjectMeta{ - Name: service.Labels[servicemesh.ApplicationNameLabel], - Namespace: metav1.NamespaceDefault, - Labels: service.Labels, + Name: applicationName, + Namespace: namespace, + Labels: labels, Annotations: map[string]string{servicemesh.ServiceMeshEnabledAnnotation: "true"}, }, Spec: v1beta1.ApplicationSpec{ @@ -193,6 +251,9 @@ func newAppliation(service *corev1.Service) *v1beta1.Application { Kind: "Deployment", }, }, + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, AddOwnerRef: true, }, } diff --git a/pkg/controller/application/application_suit_test.go b/pkg/controller/application/application_suit_test.go index 4885d1a4f..40feef74f 100644 --- a/pkg/controller/application/application_suit_test.go +++ b/pkg/controller/application/application_suit_test.go @@ -17,12 +17,15 @@ limitations under the License. package application import ( - "github.com/onsi/gomega/gexec" + "context" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/kubernetes/scheme" - "k8s.io/klog" + "k8s.io/client-go/rest" "k8s.io/klog/klogr" "kubesphere.io/kubesphere/pkg/apis" - "os" + "math/rand" "path/filepath" appv1beta1 "sigs.k8s.io/application/api/v1beta1" ctrl "sigs.k8s.io/controller-runtime" @@ -40,8 +43,8 @@ import ( // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. +var cfg *rest.Config var k8sClient client.Client -var k8sManager ctrl.Manager var testEnv *envtest.Environment func TestApplicationController(t *testing.T) { @@ -55,44 +58,23 @@ var _ = BeforeSuite(func(done Done) { logf.SetLogger(klogr.New()) By("bootstrapping test environment") - t := true - if os.Getenv("TEST_USE_EXISTING_CLUSTER") == "true" { - testEnv = &envtest.Environment{ - UseExistingCluster: &t, - } - } else { - testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crds")}, - AttachControlPlaneOutput: false, - } + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crds")}, + AttachControlPlaneOutput: false, } - sch := scheme.Scheme - err := appv1beta1.AddToScheme(sch) - Expect(err).NotTo(HaveOccurred()) - err = apis.AddToScheme(sch) - Expect(err).NotTo(HaveOccurred()) - - cfg, err := testEnv.Start() + var err error + cfg, err = testEnv.Start() Expect(err).ToNot(HaveOccurred()) Expect(cfg).ToNot(BeNil()) - k8sManager, err = ctrl.NewManager(cfg, ctrl.Options{ - Scheme: sch, - MetricsBindAddress: "0", - }) + err = appv1beta1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + err = apis.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).ToNot(HaveOccurred()) - - err = Add(k8sManager) - Expect(err).ToNot(HaveOccurred()) - - go func() { - err = k8sManager.Start(ctrl.SetupSignalHandler()) - klog.Error(err) - Expect(err).ToNot(HaveOccurred()) - }() - - k8sClient = k8sManager.GetClient() Expect(k8sClient).ToNot(BeNil()) close(done) @@ -100,7 +82,69 @@ var _ = BeforeSuite(func(done Done) { var _ = AfterSuite(func() { By("tearing down the test environment") - gexec.KillAndWait(5 * time.Second) err := testEnv.Stop() Expect(err).ToNot(HaveOccurred()) }) + +// SetupTest will setup a testing environment. +// This includes: +// * creating a Namespace to be used during the test +// * starting application controller +// * stopping application controller after the test ends +// Call this function at the start of each of your tests. +func SetupTest(ctx context.Context) *corev1.Namespace { + var stopCh chan struct{} + ns := &corev1.Namespace{} + + BeforeEach(func() { + stopCh = make(chan struct{}) + *ns = corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "testns-" + randStringRunes(5)}, + } + + err := k8sClient.Create(ctx, ns) + Expect(err).NotTo(HaveOccurred(), "failed to create a test namespace") + + mgr, err := ctrl.NewManager(cfg, ctrl.Options{}) + Expect(err).NotTo(HaveOccurred(), "failed to create a manager") + + selector, _ := labels.Parse("app.kubernetes.io/name,!kubesphere.io/creator") + + reconciler := &ApplicationReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Mapper: mgr.GetRESTMapper(), + ApplicationSelector: selector, + } + err = reconciler.SetupWithManager(mgr) + Expect(err).NotTo(HaveOccurred(), "failed to setup application reconciler") + + go func() { + err = mgr.Start(stopCh) + Expect(err).NotTo(HaveOccurred(), "failed to start manager") + }() + }) + + AfterEach(func() { + close(stopCh) + + err := k8sClient.Delete(ctx, ns) + Expect(err).NotTo(HaveOccurred(), "failed to delete test namespace") + }) + + return ns +} + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz1234567890") + +func randStringRunes(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + return string(b) +} \ No newline at end of file diff --git a/vendor/sigs.k8s.io/application/controllers/condition.go b/pkg/controller/application/condition.go similarity index 99% rename from vendor/sigs.k8s.io/application/controllers/condition.go rename to pkg/controller/application/condition.go index 5abd50ac5..e590addca 100644 --- a/vendor/sigs.k8s.io/application/controllers/condition.go +++ b/pkg/controller/application/condition.go @@ -1,7 +1,7 @@ // Copyright 2020 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 -package controllers +package application import ( corev1 "k8s.io/api/core/v1" diff --git a/vendor/sigs.k8s.io/application/controllers/status.go b/pkg/controller/application/status.go similarity index 99% rename from vendor/sigs.k8s.io/application/controllers/status.go rename to pkg/controller/application/status.go index ae375cf91..709f95322 100644 --- a/vendor/sigs.k8s.io/application/controllers/status.go +++ b/pkg/controller/application/status.go @@ -1,7 +1,7 @@ // Copyright 2020 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 -package controllers +package application import ( "strings" diff --git a/vendor/kubesphere.io/client-go b/vendor/kubesphere.io/client-go deleted file mode 120000 index 0bee05eff..000000000 --- a/vendor/kubesphere.io/client-go +++ /dev/null @@ -1 +0,0 @@ -../../staging/src/kubesphere.io/client-go \ No newline at end of file diff --git a/vendor/modules.txt b/vendor/modules.txt index e6993045b..af18ba87e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -652,7 +652,7 @@ go.uber.org/zap/internal/bufferpool go.uber.org/zap/internal/color go.uber.org/zap/internal/exit go.uber.org/zap/zapcore -# golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de => golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 +# golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 => golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 golang.org/x/crypto/bcrypt golang.org/x/crypto/blowfish golang.org/x/crypto/cast5 @@ -701,7 +701,7 @@ golang.org/x/oauth2/internal # golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 => golang.org/x/sync v0.0.0-20190423024810-112230192c58 golang.org/x/sync/errgroup golang.org/x/sync/singleflight -# golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 => golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e +# golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c => golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e golang.org/x/sys/cpu golang.org/x/sys/unix golang.org/x/sys/windows @@ -1555,7 +1555,6 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client/pkg/client sigs.k8s.io/apiserver-network-proxy/konnectivity-client/proto/client # sigs.k8s.io/application v0.8.4-0.20201016185654-c8e2959e57a0 => sigs.k8s.io/application v0.8.4-0.20201016185654-c8e2959e57a0 sigs.k8s.io/application/api/v1beta1 -sigs.k8s.io/application/controllers # sigs.k8s.io/controller-runtime v0.6.4 => sigs.k8s.io/controller-runtime v0.6.4 sigs.k8s.io/controller-runtime sigs.k8s.io/controller-runtime/pkg/builder diff --git a/vendor/sigs.k8s.io/application/controllers/application_controller.go b/vendor/sigs.k8s.io/application/controllers/application_controller.go deleted file mode 100644 index a3350bc80..000000000 --- a/vendor/sigs.k8s.io/application/controllers/application_controller.go +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package controllers - -import ( - "context" - "fmt" - - "github.com/go-logr/logr" - "k8s.io/apimachinery/pkg/api/equality" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - utilerrors "k8s.io/apimachinery/pkg/util/errors" - "k8s.io/client-go/util/retry" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - - appv1beta1 "sigs.k8s.io/application/api/v1beta1" -) - -const ( - loggerCtxKey = "logger" -) - -// ApplicationReconciler reconciles a Application object -type ApplicationReconciler struct { - client.Client - Mapper meta.RESTMapper - Log logr.Logger - Scheme *runtime.Scheme -} - -// +kubebuilder:rbac:groups=app.k8s.io,resources=applications,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=app.k8s.io,resources=applications/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=*,resources=*,verbs=list;get;update;patch;watch - -func (r *ApplicationReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { - rootCtx := context.Background() - logger := r.Log.WithValues("application", req.NamespacedName) - ctx := context.WithValue(rootCtx, loggerCtxKey, logger) - - var app appv1beta1.Application - err := r.Get(ctx, req.NamespacedName, &app) - if err != nil { - if apierrors.IsNotFound(err) { - return ctrl.Result{}, nil - } - return ctrl.Result{}, err - } - - // Application is in the process of being deleted, so no need to do anything. - if app.DeletionTimestamp != nil { - return ctrl.Result{}, nil - } - - resources, errs := r.updateComponents(ctx, &app) - newApplicationStatus := r.getNewApplicationStatus(ctx, &app, resources, &errs) - - newApplicationStatus.ObservedGeneration = app.Generation - if equality.Semantic.DeepEqual(newApplicationStatus, &app.Status) { - return ctrl.Result{}, nil - } - - err = r.updateApplicationStatus(ctx, req.NamespacedName, newApplicationStatus) - return ctrl.Result{}, err -} - -func (r *ApplicationReconciler) updateComponents(ctx context.Context, app *appv1beta1.Application) ([]*unstructured.Unstructured, []error) { - var errs []error - resources := r.fetchComponentListResources(ctx, app.Spec.ComponentGroupKinds, app.Spec.Selector, app.Namespace, &errs) - - if app.Spec.AddOwnerRef { - ownerRef := metav1.NewControllerRef(app, appv1beta1.GroupVersion.WithKind("Application")) - *ownerRef.Controller = false - if err := r.setOwnerRefForResources(ctx, *ownerRef, resources); err != nil { - errs = append(errs, err) - } - } - return resources, errs -} - -func (r *ApplicationReconciler) getNewApplicationStatus(ctx context.Context, app *appv1beta1.Application, resources []*unstructured.Unstructured, errList *[]error) *appv1beta1.ApplicationStatus { - objectStatuses := r.objectStatuses(ctx, resources, errList) - errs := utilerrors.NewAggregate(*errList) - - aggReady, countReady := aggregateReady(objectStatuses) - - newApplicationStatus := app.Status.DeepCopy() - newApplicationStatus.ComponentList = appv1beta1.ComponentList{ - Objects: objectStatuses, - } - newApplicationStatus.ComponentsReady = fmt.Sprintf("%d/%d", countReady, len(objectStatuses)) - if errs != nil { - setReadyUnknownCondition(newApplicationStatus, "ComponentsReadyUnknown", "failed to aggregate all components' statuses, check the Error condition for details") - } else if aggReady { - setReadyCondition(newApplicationStatus, "ComponentsReady", "all components ready") - } else { - setNotReadyCondition(newApplicationStatus, "ComponentsNotReady", fmt.Sprintf("%d components not ready", len(objectStatuses)-countReady)) - } - - if errs != nil { - setErrorCondition(newApplicationStatus, "ErrorSeen", errs.Error()) - } else { - clearErrorCondition(newApplicationStatus) - } - - return newApplicationStatus -} - -func (r *ApplicationReconciler) fetchComponentListResources(ctx context.Context, groupKinds []metav1.GroupKind, selector *metav1.LabelSelector, namespace string, errs *[]error) []*unstructured.Unstructured { - logger := getLoggerOrDie(ctx) - var resources []*unstructured.Unstructured - - if selector == nil { - logger.Info("No selector is specified") - return resources - } - - for _, gk := range groupKinds { - mapping, err := r.Mapper.RESTMapping(schema.GroupKind{ - Group: appv1beta1.StripVersion(gk.Group), - Kind: gk.Kind, - }) - if err != nil { - logger.Info("NoMappingForGK", "gk", gk.String()) - continue - } - - list := &unstructured.UnstructuredList{} - list.SetGroupVersionKind(mapping.GroupVersionKind) - if err = r.Client.List(ctx, list, client.InNamespace(namespace), client.MatchingLabels(selector.MatchLabels)); err != nil { - logger.Error(err, "unable to list resources for GVK", "gvk", mapping.GroupVersionKind) - *errs = append(*errs, err) - continue - } - - for _, u := range list.Items { - resource := u - resources = append(resources, &resource) - } - } - return resources -} - -func (r *ApplicationReconciler) setOwnerRefForResources(ctx context.Context, ownerRef metav1.OwnerReference, resources []*unstructured.Unstructured) error { - logger := getLoggerOrDie(ctx) - for _, resource := range resources { - ownerRefs := resource.GetOwnerReferences() - ownerRefFound := false - for i, refs := range ownerRefs { - if ownerRef.Kind == refs.Kind && - ownerRef.APIVersion == refs.APIVersion && - ownerRef.Name == refs.Name { - ownerRefFound = true - if ownerRef.UID != refs.UID { - ownerRefs[i] = ownerRef - } - } - } - - if !ownerRefFound { - ownerRefs = append(ownerRefs, ownerRef) - } - resource.SetOwnerReferences(ownerRefs) - err := r.Client.Update(ctx, resource) - if err != nil { - // We log this error, but we continue and try to set the ownerRefs on the other resources. - logger.Error(err, "ErrorSettingOwnerRef", "gvk", resource.GroupVersionKind().String(), - "namespace", resource.GetNamespace(), "name", resource.GetName()) - } - } - return nil -} - -func (r *ApplicationReconciler) objectStatuses(ctx context.Context, resources []*unstructured.Unstructured, errs *[]error) []appv1beta1.ObjectStatus { - logger := getLoggerOrDie(ctx) - var objectStatuses []appv1beta1.ObjectStatus - for _, resource := range resources { - os := appv1beta1.ObjectStatus{ - Group: resource.GroupVersionKind().Group, - Kind: resource.GetKind(), - Name: resource.GetName(), - Link: resource.GetSelfLink(), - } - s, err := status(resource) - if err != nil { - logger.Error(err, "unable to compute status for resource", "gvk", resource.GroupVersionKind().String(), - "namespace", resource.GetNamespace(), "name", resource.GetName()) - *errs = append(*errs, err) - } - os.Status = s - objectStatuses = append(objectStatuses, os) - } - return objectStatuses -} - -func aggregateReady(objectStatuses []appv1beta1.ObjectStatus) (bool, int) { - countReady := 0 - for _, os := range objectStatuses { - if os.Status == StatusReady { - countReady++ - } - } - if countReady == len(objectStatuses) { - return true, countReady - } - return false, countReady -} - -func (r *ApplicationReconciler) updateApplicationStatus(ctx context.Context, nn types.NamespacedName, status *appv1beta1.ApplicationStatus) error { - if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - original := &appv1beta1.Application{} - if err := r.Get(ctx, nn, original); err != nil { - return err - } - original.Status = *status - if err := r.Client.Status().Update(ctx, original); err != nil { - return err - } - return nil - }); err != nil { - return fmt.Errorf("failed to update status of Application %s/%s: %v", nn.Namespace, nn.Name, err) - } - return nil -} - -func (r *ApplicationReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&appv1beta1.Application{}). - Complete(r) -} - -func getLoggerOrDie(ctx context.Context) logr.Logger { - logger, ok := ctx.Value(loggerCtxKey).(logr.Logger) - if !ok { - panic("context didn't contain logger") - } - return logger -} From a2ede11ee7e74b8edfcb57484f7ac1fdf81bf60d Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 1 Mar 2021 15:21:15 +0800 Subject: [PATCH 15/21] fix code format issues Signed-off-by: Jeff --- .../application_controller_test.go | 20 +++++++++---------- .../application/application_suit_test.go | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pkg/controller/application/application_controller_test.go b/pkg/controller/application/application_controller_test.go index e81d2755a..532306ffb 100644 --- a/pkg/controller/application/application_controller_test.go +++ b/pkg/controller/application/application_controller_test.go @@ -35,7 +35,7 @@ import ( const ( applicationName = "bookinfo" - serviceName = "productpage" + serviceName = "productpage" timeout = time.Second * 30 interval = time.Second * 2 ) @@ -48,7 +48,7 @@ var _ = Context("Inside of a new namespace", func() { Describe("Application", func() { applicationLabels := map[string]string{ - "app.kubernetes.io/name": "bookinfo", + "app.kubernetes.io/name": "bookinfo", "app.kubernetes.io/version": "1", } @@ -79,7 +79,7 @@ var _ = Context("Inside of a new namespace", func() { newApp.Labels["kubesphere.io/creator"] = "" } - updated, err := updateWithRetries(k8sClient, ctx, application.Namespace, applicationName, updateApplication, 1 * time.Second, 5 * time.Second) + updated, err := updateWithRetries(k8sClient, ctx, application.Namespace, applicationName, updateApplication, 1*time.Second, 5*time.Second) Expect(updated).Should(BeTrue()) Eventually(func() bool { @@ -112,7 +112,7 @@ var _ = Context("Inside of a new namespace", func() { type UpdateObjectFunc func(obj interface{}) -func updateWithRetries(client client.Client, ctx context.Context, namespace, name string, updateFunc UpdateObjectFunc, interval, timeout time.Duration)(bool, error) { +func updateWithRetries(client client.Client, ctx context.Context, namespace, name string, updateFunc UpdateObjectFunc, interval, timeout time.Duration) (bool, error) { var updateErr error pollErr := wait.PollImmediate(interval, timeout, func() (done bool, err error) { @@ -143,9 +143,9 @@ func newDeployments(deploymentName, namespace string, labels map[string]string, deployment := &v1.Deployment{ ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s-%s", deploymentName, version), - Namespace: namespace, - Labels: labels, + Name: fmt.Sprintf("%s-%s", deploymentName, version), + Namespace: namespace, + Labels: labels, Annotations: map[string]string{servicemesh.ServiceMeshEnabledAnnotation: "true"}, }, Spec: v1.DeploymentSpec{ @@ -155,7 +155,7 @@ func newDeployments(deploymentName, namespace string, labels map[string]string, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: labels, + Labels: labels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ @@ -201,7 +201,7 @@ func newService(serviceName, namesapce string, labels map[string]string) *corev1 ObjectMeta: metav1.ObjectMeta{ Name: serviceName, Namespace: namesapce, - Labels: labels, + Labels: labels, Annotations: map[string]string{ "servicemesh.kubesphere.io/enabled": "true", }, @@ -225,7 +225,7 @@ func newService(serviceName, namesapce string, labels map[string]string) *corev1 }, }, Selector: labels, - Type: corev1.ServiceTypeClusterIP, + Type: corev1.ServiceTypeClusterIP, }, Status: corev1.ServiceStatus{}, } diff --git a/pkg/controller/application/application_suit_test.go b/pkg/controller/application/application_suit_test.go index 40feef74f..690d1ac42 100644 --- a/pkg/controller/application/application_suit_test.go +++ b/pkg/controller/application/application_suit_test.go @@ -111,9 +111,9 @@ func SetupTest(ctx context.Context) *corev1.Namespace { selector, _ := labels.Parse("app.kubernetes.io/name,!kubesphere.io/creator") reconciler := &ApplicationReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Mapper: mgr.GetRESTMapper(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Mapper: mgr.GetRESTMapper(), ApplicationSelector: selector, } err = reconciler.SetupWithManager(mgr) @@ -147,4 +147,4 @@ func randStringRunes(n int) string { b[i] = letterRunes[rand.Intn(len(letterRunes))] } return string(b) -} \ No newline at end of file +} From 70fa24010c526c92fbbae25250a6de2def78c54c Mon Sep 17 00:00:00 2001 From: hongming Date: Wed, 27 Jan 2021 11:06:23 +0800 Subject: [PATCH 16/21] support workspace resource quota Signed-off-by: hongming --- cmd/controller-manager/app/server.go | 33 +- .../quota.kubesphere.io_resourcequotas.yaml | 170 +++++ config/webhook/iam.yaml | 21 +- config/webhook/ks-controller-manager.yaml | 13 + config/webhook/{nsnp.yaml => network.yaml} | 4 +- config/webhook/quota.yaml | 30 + go.mod | 2 +- hack/generate_client.sh | 2 +- kube/pkg/api/v1/pod/util.go | 336 ++++++++ kube/pkg/apis/core/helper/helpers.go | 51 ++ kube/pkg/apis/core/v1/helper/helpers.go | 500 ++++++++++++ kube/pkg/apis/core/v1/helper/qos/qos.go | 103 +++ kube/pkg/features/features.go | 680 +++++++++++++++++ .../core/persistent_volume_claims.go | 188 +++++ kube/pkg/quota/v1/evaluator/core/pods.go | 397 ++++++++++ kube/pkg/quota/v1/evaluator/core/registry.go | 49 ++ kube/pkg/quota/v1/evaluator/core/services.go | 149 ++++ kube/pkg/quota/v1/generic/configuration.go | 44 ++ kube/pkg/quota/v1/generic/evaluator.go | 319 ++++++++ kube/pkg/quota/v1/generic/registry.go | 81 ++ kube/pkg/quota/v1/install/registry.go | 47 ++ kube/pkg/quota/v1/interfaces.go | 88 +++ kube/pkg/quota/v1/resources.go | 293 +++++++ .../pkg/admission/resourcequota/admission.go | 126 +++ .../resourcequota/apis/resourcequota/types.go | 74 ++ .../pkg/admission/resourcequota/controller.go | 717 ++++++++++++++++++ .../resourcequota/resource_access.go | 155 ++++ pkg/apis/addtoscheme_quota_v1alpha2.go | 26 + pkg/apis/quota/group.go | 18 + pkg/apis/quota/v1alpha2/doc.go | 23 + pkg/apis/quota/v1alpha2/register.go | 46 ++ pkg/apis/quota/v1alpha2/types.go | 99 +++ pkg/apis/quota/v1alpha2/types_test.go | 64 ++ .../quota/v1alpha2/v1alpha2_suite_test.go | 55 ++ .../quota/v1alpha2/zz_generated.deepcopy.go | 167 ++++ pkg/client/clientset/versioned/clientset.go | 14 + .../versioned/fake/clientset_generated.go | 7 + .../clientset/versioned/fake/register.go | 2 + .../clientset/versioned/scheme/register.go | 2 + .../versioned/typed/quota/v1alpha2/doc.go | 20 + .../typed/quota/v1alpha2/fake/doc.go | 20 + .../quota/v1alpha2/fake/fake_quota_client.go | 40 + .../quota/v1alpha2/fake/fake_resourcequota.go | 133 ++++ .../quota/v1alpha2/generated_expansion.go | 21 + .../typed/quota/v1alpha2/quota_client.go | 89 +++ .../typed/quota/v1alpha2/resourcequota.go | 184 +++++ .../informers/externalversions/factory.go | 6 + .../informers/externalversions/generic.go | 5 + .../externalversions/quota/interface.go | 46 ++ .../quota/v1alpha2/interface.go | 45 ++ .../quota/v1alpha2/resourcequota.go | 89 +++ .../quota/v1alpha2/expansion_generated.go | 23 + .../listers/quota/v1alpha2/resourcequota.go | 65 ++ pkg/controller/quota/accessor.go | 206 +++++ pkg/controller/quota/lockfactory.go | 59 ++ .../quota/resourcequota_controller.go | 299 ++++++++ pkg/controller/quota/resourcequota_webhook.go | 191 +++++ pkg/controller/quota/util.go | 92 +++ pkg/kapis/tenant/v1alpha2/handler.go | 76 ++ pkg/kapis/tenant/v1alpha2/register.go | 33 + pkg/models/tenant/resourcequota.go | 75 ++ pkg/models/tenant/tenant.go | 5 + .../metadata/metadatainformer/informer.go | 157 ++++ .../metadata/metadatainformer/interface.go | 34 + .../metadata/metadatalister/interface.go | 40 + .../metadata/metadatalister/lister.go | 91 +++ .../client-go/metadata/metadatalister/shim.go | 87 +++ vendor/modules.txt | 2 + 68 files changed, 7397 insertions(+), 31 deletions(-) create mode 100644 config/crds/quota.kubesphere.io_resourcequotas.yaml create mode 100644 config/webhook/ks-controller-manager.yaml rename config/webhook/{nsnp.yaml => network.yaml} (82%) create mode 100644 config/webhook/quota.yaml create mode 100644 kube/pkg/api/v1/pod/util.go create mode 100644 kube/pkg/apis/core/helper/helpers.go create mode 100644 kube/pkg/apis/core/v1/helper/helpers.go create mode 100644 kube/pkg/apis/core/v1/helper/qos/qos.go create mode 100644 kube/pkg/features/features.go create mode 100644 kube/pkg/quota/v1/evaluator/core/persistent_volume_claims.go create mode 100644 kube/pkg/quota/v1/evaluator/core/pods.go create mode 100644 kube/pkg/quota/v1/evaluator/core/registry.go create mode 100644 kube/pkg/quota/v1/evaluator/core/services.go create mode 100644 kube/pkg/quota/v1/generic/configuration.go create mode 100644 kube/pkg/quota/v1/generic/evaluator.go create mode 100644 kube/pkg/quota/v1/generic/registry.go create mode 100644 kube/pkg/quota/v1/install/registry.go create mode 100644 kube/pkg/quota/v1/interfaces.go create mode 100644 kube/pkg/quota/v1/resources.go create mode 100644 kube/plugin/pkg/admission/resourcequota/admission.go create mode 100644 kube/plugin/pkg/admission/resourcequota/apis/resourcequota/types.go create mode 100644 kube/plugin/pkg/admission/resourcequota/controller.go create mode 100644 kube/plugin/pkg/admission/resourcequota/resource_access.go create mode 100644 pkg/apis/addtoscheme_quota_v1alpha2.go create mode 100644 pkg/apis/quota/group.go create mode 100644 pkg/apis/quota/v1alpha2/doc.go create mode 100644 pkg/apis/quota/v1alpha2/register.go create mode 100644 pkg/apis/quota/v1alpha2/types.go create mode 100644 pkg/apis/quota/v1alpha2/types_test.go create mode 100644 pkg/apis/quota/v1alpha2/v1alpha2_suite_test.go create mode 100644 pkg/apis/quota/v1alpha2/zz_generated.deepcopy.go create mode 100644 pkg/client/clientset/versioned/typed/quota/v1alpha2/doc.go create mode 100644 pkg/client/clientset/versioned/typed/quota/v1alpha2/fake/doc.go create mode 100644 pkg/client/clientset/versioned/typed/quota/v1alpha2/fake/fake_quota_client.go create mode 100644 pkg/client/clientset/versioned/typed/quota/v1alpha2/fake/fake_resourcequota.go create mode 100644 pkg/client/clientset/versioned/typed/quota/v1alpha2/generated_expansion.go create mode 100644 pkg/client/clientset/versioned/typed/quota/v1alpha2/quota_client.go create mode 100644 pkg/client/clientset/versioned/typed/quota/v1alpha2/resourcequota.go create mode 100644 pkg/client/informers/externalversions/quota/interface.go create mode 100644 pkg/client/informers/externalversions/quota/v1alpha2/interface.go create mode 100644 pkg/client/informers/externalversions/quota/v1alpha2/resourcequota.go create mode 100644 pkg/client/listers/quota/v1alpha2/expansion_generated.go create mode 100644 pkg/client/listers/quota/v1alpha2/resourcequota.go create mode 100644 pkg/controller/quota/accessor.go create mode 100644 pkg/controller/quota/lockfactory.go create mode 100644 pkg/controller/quota/resourcequota_controller.go create mode 100644 pkg/controller/quota/resourcequota_webhook.go create mode 100644 pkg/controller/quota/util.go create mode 100644 pkg/models/tenant/resourcequota.go create mode 100644 vendor/k8s.io/client-go/metadata/metadatainformer/informer.go create mode 100644 vendor/k8s.io/client-go/metadata/metadatainformer/interface.go create mode 100644 vendor/k8s.io/client-go/metadata/metadatalister/interface.go create mode 100644 vendor/k8s.io/client-go/metadata/metadatalister/lister.go create mode 100644 vendor/k8s.io/client-go/metadata/metadatalister/shim.go diff --git a/cmd/controller-manager/app/server.go b/cmd/controller-manager/app/server.go index e93c87065..2434dab33 100644 --- a/cmd/controller-manager/app/server.go +++ b/cmd/controller-manager/app/server.go @@ -18,6 +18,7 @@ package app import ( "fmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "kubesphere.io/kubesphere/pkg/controller/application" "os" @@ -32,6 +33,7 @@ import ( controllerconfig "kubesphere.io/kubesphere/pkg/apiserver/config" "kubesphere.io/kubesphere/pkg/controller/namespace" "kubesphere.io/kubesphere/pkg/controller/network/webhooks" + "kubesphere.io/kubesphere/pkg/controller/quota" "kubesphere.io/kubesphere/pkg/controller/serviceaccount" "kubesphere.io/kubesphere/pkg/controller/user" "kubesphere.io/kubesphere/pkg/controller/workspace" @@ -194,29 +196,32 @@ func run(s *options.KubeSphereControllerManagerOptions, stopCh <-chan struct{}) klog.Fatalf("unable add APIs to scheme: %v", err) } + // register common meta types into schemas. + metav1.AddToGroupVersion(mgr.GetScheme(), metav1.SchemeGroupVersion) + workspaceTemplateReconciler := &workspacetemplate.Reconciler{MultiClusterEnabled: s.MultiClusterOptions.Enable} if err = workspaceTemplateReconciler.SetupWithManager(mgr); err != nil { - klog.Fatal("Unable to create workspace template controller") + klog.Fatalf("Unable to create workspace template controller: %v", err) } workspaceReconciler := &workspace.Reconciler{} if err = workspaceReconciler.SetupWithManager(mgr); err != nil { - klog.Fatal("Unable to create workspace controller") + klog.Fatalf("Unable to create workspace controller: %v", err) } workspaceRoleReconciler := &workspacerole.Reconciler{MultiClusterEnabled: s.MultiClusterOptions.Enable} if err = workspaceRoleReconciler.SetupWithManager(mgr); err != nil { - klog.Fatal("Unable to create workspace role controller") + klog.Fatalf("Unable to create workspace role controller: %v", err) } workspaceRoleBindingReconciler := &workspacerolebinding.Reconciler{MultiClusterEnabled: s.MultiClusterOptions.Enable} if err = workspaceRoleBindingReconciler.SetupWithManager(mgr); err != nil { - klog.Fatal("Unable to create workspace role binding controller") + klog.Fatalf("Unable to create workspace role binding controller: %v", err) } namespaceReconciler := &namespace.Reconciler{} if err = namespaceReconciler.SetupWithManager(mgr); err != nil { - klog.Fatal("Unable to create namespace controller") + klog.Fatalf("Unable to create namespace controller: %v", err) } selector, _ := labels.Parse(s.ApplicationSelector) @@ -227,13 +232,17 @@ func run(s *options.KubeSphereControllerManagerOptions, stopCh <-chan struct{}) ApplicationSelector: selector, } if err = applicationReconciler.SetupWithManager(mgr); err != nil { - klog.Fatal("Unable to create application controller") + klog.Fatalf("Unable to create application controller: %v", err) } saReconciler := &serviceaccount.Reconciler{} - if err = saReconciler.SetupWithManager(mgr); err != nil { - klog.Fatal("Unable to create ServiceAccount controller") + klog.Fatalf("Unable to create ServiceAccount controller: %v", err) + } + + resourceQuotaReconciler := quota.Reconciler{} + if err := resourceQuotaReconciler.SetupWithManager(mgr, quota.DefaultMaxConcurrentReconciles, quota.DefaultResyncPeriod, informerFactory.KubernetesSharedInformerFactory()); err != nil { + klog.Fatalf("Unable to create ResourceQuota controller: %v", err) } // TODO(jeff): refactor config with CRD @@ -263,10 +272,16 @@ func run(s *options.KubeSphereControllerManagerOptions, stopCh <-chan struct{}) hookServer := mgr.GetWebhookServer() klog.V(2).Info("registering webhooks to the webhook server") - hookServer.Register("/validate-email-iam-kubesphere-io-v1alpha2-user", &webhook.Admission{Handler: &user.EmailValidator{Client: mgr.GetClient()}}) + hookServer.Register("/validate-email-iam-kubesphere-io-v1alpha2", &webhook.Admission{Handler: &user.EmailValidator{Client: mgr.GetClient()}}) hookServer.Register("/validate-network-kubesphere-io-v1alpha1", &webhook.Admission{Handler: &webhooks.ValidatingHandler{C: mgr.GetClient()}}) hookServer.Register("/mutate-network-kubesphere-io-v1alpha1", &webhook.Admission{Handler: &webhooks.MutatingHandler{C: mgr.GetClient()}}) + resourceQuotaAdmission, err := quota.NewResourceQuotaAdmission(mgr.GetClient(), mgr.GetScheme()) + if err != nil { + klog.Fatalf("unable to create resource quota admission: %v", err) + } + hookServer.Register("/validate-quota-kubesphere-io-v1alpha2", &webhook.Admission{Handler: resourceQuotaAdmission}) + klog.V(2).Info("registering metrics to the webhook server") hookServer.Register("/metrics", metrics.Handler()) diff --git a/config/crds/quota.kubesphere.io_resourcequotas.yaml b/config/crds/quota.kubesphere.io_resourcequotas.yaml new file mode 100644 index 000000000..4b1ba01b6 --- /dev/null +++ b/config/crds/quota.kubesphere.io_resourcequotas.yaml @@ -0,0 +1,170 @@ + +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: (devel) + creationTimestamp: null + name: resourcequotas.quota.kubesphere.io +spec: + group: quota.kubesphere.io + names: + categories: + - quota + kind: ResourceQuota + listKind: ResourceQuotaList + plural: resourcequotas + singular: resourcequota + scope: Cluster + subresources: + status: {} + validation: + openAPIV3Schema: + description: WorkspaceResourceQuota sets aggregate quota restrictions enforced + per workspace + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + description: 'Standard object''s metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata' + type: object + spec: + description: Spec defines the desired quota + properties: + quota: + description: Quota defines the desired quota + properties: + hard: + additionalProperties: + type: string + description: 'hard is the set of desired hard limits for each named + resource. More info: https://kubernetes.io/docs/concepts/policy/resource-quotas/' + type: object + scopeSelector: + description: scopeSelector is also a collection of filters like + scopes that must match each object tracked by a quota but expressed + using ScopeSelectorOperator in combination with possible values. + For a resource to match, both scopes AND scopeSelector (if specified + in spec), must be matched. + properties: + matchExpressions: + description: A list of scope selector requirements by scope + of the resources. + items: + description: A scoped-resource selector requirement is a selector + that contains values, a scope name, and an operator that + relates the scope name and values. + properties: + operator: + description: Represents a scope's relationship to a set + of values. Valid operators are In, NotIn, Exists, DoesNotExist. + type: string + scopeName: + description: The name of the scope that the selector applies + to. + type: string + values: + description: An array of string values. If the operator + is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. This array is replaced during a + strategic merge patch. + items: + type: string + type: array + required: + - operator + - scopeName + type: object + type: array + type: object + scopes: + description: A collection of filters that must match each object + tracked by a quota. If not specified, the quota matches all objects. + items: + description: A ResourceQuotaScope defines a filter that must match + each object tracked by a quota + type: string + type: array + type: object + selector: + additionalProperties: + type: string + description: LabelSelector is used to select projects by label. + type: object + required: + - quota + - selector + type: object + status: + description: Status defines the actual enforced quota and its current usage + properties: + namespaces: + description: Namespaces slices the usage by project. + items: + description: ResourceQuotaStatusByNamespace gives status for a particular + project + properties: + hard: + additionalProperties: + type: string + description: 'Hard is the set of enforced hard limits for each + named resource. More info: https://kubernetes.io/docs/concepts/policy/resource-quotas/' + type: object + namespace: + description: Namespace the project this status applies to + type: string + used: + additionalProperties: + type: string + description: Used is the current observed total usage of the resource + in the namespace. + type: object + required: + - namespace + type: object + type: array + total: + description: Total defines the actual enforced quota and its current + usage across all projects + properties: + hard: + additionalProperties: + type: string + description: 'Hard is the set of enforced hard limits for each named + resource. More info: https://kubernetes.io/docs/concepts/policy/resource-quotas/' + type: object + used: + additionalProperties: + type: string + description: Used is the current observed total usage of the resource + in the namespace. + type: object + type: object + required: + - namespaces + - total + type: object + required: + - spec + type: object + version: v1alpha2 + versions: + - name: v1alpha2 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/webhook/iam.yaml b/config/webhook/iam.yaml index 58eae0b52..3f72946ec 100644 --- a/config/webhook/iam.yaml +++ b/config/webhook/iam.yaml @@ -8,9 +8,9 @@ webhooks: clientConfig: caBundle: service: - name: webhook-service + name: ks-controller-manager namespace: kubesphere-system - path: /validate-email-iam-kubesphere-io-v1alpha2-user + path: /validate-email-iam-kubesphere-io-v1alpha2 failurePolicy: Fail name: vemail.iam.kubesphere.io rules: @@ -22,19 +22,4 @@ webhooks: - CREATE - UPDATE resources: - - users - ---- - -apiVersion: v1 -kind: Service -metadata: - name: webhook-service - namespace: kubesphere-system -spec: - ports: - - port: 443 - targetPort: 443 - selector: - app: ks-controller-manager - tier: backend \ No newline at end of file + - users \ No newline at end of file diff --git a/config/webhook/ks-controller-manager.yaml b/config/webhook/ks-controller-manager.yaml new file mode 100644 index 000000000..5cc157e62 --- /dev/null +++ b/config/webhook/ks-controller-manager.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: ks-controller-manager + namespace: kubesphere-system +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 8443 + selector: + app: ks-controller-manager + tier: backend \ No newline at end of file diff --git a/config/webhook/nsnp.yaml b/config/webhook/network.yaml similarity index 82% rename from config/webhook/nsnp.yaml rename to config/webhook/network.yaml index 48aaec0b9..aeb562f4c 100644 --- a/config/webhook/nsnp.yaml +++ b/config/webhook/network.yaml @@ -7,9 +7,9 @@ webhooks: - clientConfig: caBundle: service: - name: kubesphere-controller-manager-service + name: ks-controller-manager namespace: kubesphere-system - path: /validate-nsnp-kubesphere-io-v1alpha1-network + path: /validate-network-kubesphere-io-v1alpha1 failurePolicy: Fail name: validate.nsnp.kubesphere.io rules: diff --git a/config/webhook/quota.yaml b/config/webhook/quota.yaml new file mode 100644 index 000000000..51236c126 --- /dev/null +++ b/config/webhook/quota.yaml @@ -0,0 +1,30 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: resourcesquotas.quota.kubesphere.io +webhooks: + - admissionReviewVersions: + - v1beta1 + clientConfig: + caBundle: + service: + name: ks-controller-manager + namespace: kubesphere-system + path: /validate-quota-kubesphere-io-v1alpha2 + port: 443 + failurePolicy: Ignore + matchPolicy: Exact + name: resourcesquotas.quota.kubesphere.io + namespaceSelector: {} + objectSelector: {} + rules: + - apiGroups: + - '*' + apiVersions: + - '*' + operations: + - CREATE + resources: + - pods + scope: '*' + sideEffects: None \ No newline at end of file diff --git a/go.mod b/go.mod index 5c3196d49..eb1afff07 100644 --- a/go.mod +++ b/go.mod @@ -43,6 +43,7 @@ require ( github.com/google/go-cmp v0.5.0 github.com/google/uuid v1.1.1 github.com/gorilla/websocket v1.4.1 + github.com/hashicorp/golang-lru v0.5.4 github.com/json-iterator/go v1.1.10 github.com/kelseyhightower/envconfig v1.4.0 // indirect github.com/kr/text v0.2.0 // indirect @@ -733,7 +734,6 @@ replace ( gopkg.in/tchap/go-patricia.v2 => gopkg.in/tchap/go-patricia.v2 v2.2.6 gopkg.in/tomb.v1 => gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 gopkg.in/warnings.v0 => gopkg.in/warnings.v0 v0.1.2 - gopkg.in/yaml.v1 => gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 gopkg.in/yaml.v2 => gopkg.in/yaml.v2 v2.3.0 gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c gotest.tools => gotest.tools v2.2.0+incompatible diff --git a/hack/generate_client.sh b/hack/generate_client.sh index 94ae37d3f..fced9d970 100755 --- a/hack/generate_client.sh +++ b/hack/generate_client.sh @@ -2,7 +2,7 @@ set -e -GV="network:v1alpha1 servicemesh:v1alpha2 tenant:v1alpha1 tenant:v1alpha2 devops:v1alpha1 iam:v1alpha2 devops:v1alpha3 cluster:v1alpha1 storage:v1alpha1 auditing:v1alpha1 types:v1beta1" +GV="network:v1alpha1 servicemesh:v1alpha2 tenant:v1alpha1 tenant:v1alpha2 devops:v1alpha1 iam:v1alpha2 devops:v1alpha3 cluster:v1alpha1 storage:v1alpha1 auditing:v1alpha1 types:v1beta1 quota:v1alpha2" rm -rf ./pkg/client ./hack/generate_group.sh "client,lister,informer" kubesphere.io/kubesphere/pkg/client kubesphere.io/kubesphere/pkg/apis "$GV" --output-base=./ -h "$PWD/hack/boilerplate.go.txt" diff --git a/kube/pkg/api/v1/pod/util.go b/kube/pkg/api/v1/pod/util.go new file mode 100644 index 000000000..6825dfe29 --- /dev/null +++ b/kube/pkg/api/v1/pod/util.go @@ -0,0 +1,336 @@ +/* +Copyright 2015 The Kubernetes 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. +*/ + +package pod + +import ( + "fmt" + "time" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + utilfeature "k8s.io/apiserver/pkg/util/feature" + "kubesphere.io/kubesphere/kube/pkg/features" +) + +// FindPort locates the container port for the given pod and portName. If the +// targetPort is a number, use that. If the targetPort is a string, look that +// string up in all named ports in all containers in the target pod. If no +// match is found, fail. +func FindPort(pod *v1.Pod, svcPort *v1.ServicePort) (int, error) { + portName := svcPort.TargetPort + switch portName.Type { + case intstr.String: + name := portName.StrVal + for _, container := range pod.Spec.Containers { + for _, port := range container.Ports { + if port.Name == name && port.Protocol == svcPort.Protocol { + return int(port.ContainerPort), nil + } + } + } + case intstr.Int: + return portName.IntValue(), nil + } + + return 0, fmt.Errorf("no suitable port for manifest: %s", pod.UID) +} + +// ContainerVisitor is called with each container spec, and returns true +// if visiting should continue. +type ContainerVisitor func(container *v1.Container) (shouldContinue bool) + +// VisitContainers invokes the visitor function with a pointer to the container +// spec of every container in the given pod spec. If visitor returns false, +// visiting is short-circuited. VisitContainers returns true if visiting completes, +// false if visiting was short-circuited. +func VisitContainers(podSpec *v1.PodSpec, visitor ContainerVisitor) bool { + for i := range podSpec.InitContainers { + if !visitor(&podSpec.InitContainers[i]) { + return false + } + } + for i := range podSpec.Containers { + if !visitor(&podSpec.Containers[i]) { + return false + } + } + if utilfeature.DefaultFeatureGate.Enabled(features.EphemeralContainers) { + for i := range podSpec.EphemeralContainers { + if !visitor((*v1.Container)(&podSpec.EphemeralContainers[i].EphemeralContainerCommon)) { + return false + } + } + } + return true +} + +// Visitor is called with each object name, and returns true if visiting should continue +type Visitor func(name string) (shouldContinue bool) + +// VisitPodSecretNames invokes the visitor function with the name of every secret +// referenced by the pod spec. If visitor returns false, visiting is short-circuited. +// Transitive references (e.g. pod -> pvc -> pv -> secret) are not visited. +// Returns true if visiting completed, false if visiting was short-circuited. +func VisitPodSecretNames(pod *v1.Pod, visitor Visitor) bool { + for _, reference := range pod.Spec.ImagePullSecrets { + if !visitor(reference.Name) { + return false + } + } + VisitContainers(&pod.Spec, func(c *v1.Container) bool { + return visitContainerSecretNames(c, visitor) + }) + var source *v1.VolumeSource + + for i := range pod.Spec.Volumes { + source = &pod.Spec.Volumes[i].VolumeSource + switch { + case source.AzureFile != nil: + if len(source.AzureFile.SecretName) > 0 && !visitor(source.AzureFile.SecretName) { + return false + } + case source.CephFS != nil: + if source.CephFS.SecretRef != nil && !visitor(source.CephFS.SecretRef.Name) { + return false + } + case source.Cinder != nil: + if source.Cinder.SecretRef != nil && !visitor(source.Cinder.SecretRef.Name) { + return false + } + case source.FlexVolume != nil: + if source.FlexVolume.SecretRef != nil && !visitor(source.FlexVolume.SecretRef.Name) { + return false + } + case source.Projected != nil: + for j := range source.Projected.Sources { + if source.Projected.Sources[j].Secret != nil { + if !visitor(source.Projected.Sources[j].Secret.Name) { + return false + } + } + } + case source.RBD != nil: + if source.RBD.SecretRef != nil && !visitor(source.RBD.SecretRef.Name) { + return false + } + case source.Secret != nil: + if !visitor(source.Secret.SecretName) { + return false + } + case source.ScaleIO != nil: + if source.ScaleIO.SecretRef != nil && !visitor(source.ScaleIO.SecretRef.Name) { + return false + } + case source.ISCSI != nil: + if source.ISCSI.SecretRef != nil && !visitor(source.ISCSI.SecretRef.Name) { + return false + } + case source.StorageOS != nil: + if source.StorageOS.SecretRef != nil && !visitor(source.StorageOS.SecretRef.Name) { + return false + } + case source.CSI != nil: + if source.CSI.NodePublishSecretRef != nil && !visitor(source.CSI.NodePublishSecretRef.Name) { + return false + } + } + } + return true +} + +func visitContainerSecretNames(container *v1.Container, visitor Visitor) bool { + for _, env := range container.EnvFrom { + if env.SecretRef != nil { + if !visitor(env.SecretRef.Name) { + return false + } + } + } + for _, envVar := range container.Env { + if envVar.ValueFrom != nil && envVar.ValueFrom.SecretKeyRef != nil { + if !visitor(envVar.ValueFrom.SecretKeyRef.Name) { + return false + } + } + } + return true +} + +// VisitPodConfigmapNames invokes the visitor function with the name of every configmap +// referenced by the pod spec. If visitor returns false, visiting is short-circuited. +// Transitive references (e.g. pod -> pvc -> pv -> secret) are not visited. +// Returns true if visiting completed, false if visiting was short-circuited. +func VisitPodConfigmapNames(pod *v1.Pod, visitor Visitor) bool { + VisitContainers(&pod.Spec, func(c *v1.Container) bool { + return visitContainerConfigmapNames(c, visitor) + }) + var source *v1.VolumeSource + for i := range pod.Spec.Volumes { + source = &pod.Spec.Volumes[i].VolumeSource + switch { + case source.Projected != nil: + for j := range source.Projected.Sources { + if source.Projected.Sources[j].ConfigMap != nil { + if !visitor(source.Projected.Sources[j].ConfigMap.Name) { + return false + } + } + } + case source.ConfigMap != nil: + if !visitor(source.ConfigMap.Name) { + return false + } + } + } + return true +} + +func visitContainerConfigmapNames(container *v1.Container, visitor Visitor) bool { + for _, env := range container.EnvFrom { + if env.ConfigMapRef != nil { + if !visitor(env.ConfigMapRef.Name) { + return false + } + } + } + for _, envVar := range container.Env { + if envVar.ValueFrom != nil && envVar.ValueFrom.ConfigMapKeyRef != nil { + if !visitor(envVar.ValueFrom.ConfigMapKeyRef.Name) { + return false + } + } + } + return true +} + +// GetContainerStatus extracts the status of container "name" from "statuses". +// It also returns if "name" exists. +func GetContainerStatus(statuses []v1.ContainerStatus, name string) (v1.ContainerStatus, bool) { + for i := range statuses { + if statuses[i].Name == name { + return statuses[i], true + } + } + return v1.ContainerStatus{}, false +} + +// GetExistingContainerStatus extracts the status of container "name" from "statuses", +// It also returns if "name" exists. +func GetExistingContainerStatus(statuses []v1.ContainerStatus, name string) v1.ContainerStatus { + status, _ := GetContainerStatus(statuses, name) + return status +} + +// IsPodAvailable returns true if a pod is available; false otherwise. +// Precondition for an available pod is that it must be ready. On top +// of that, there are two cases when a pod can be considered available: +// 1. minReadySeconds == 0, or +// 2. LastTransitionTime (is set) + minReadySeconds < current time +func IsPodAvailable(pod *v1.Pod, minReadySeconds int32, now metav1.Time) bool { + if !IsPodReady(pod) { + return false + } + + c := GetPodReadyCondition(pod.Status) + minReadySecondsDuration := time.Duration(minReadySeconds) * time.Second + if minReadySeconds == 0 || !c.LastTransitionTime.IsZero() && c.LastTransitionTime.Add(minReadySecondsDuration).Before(now.Time) { + return true + } + return false +} + +// IsPodReady returns true if a pod is ready; false otherwise. +func IsPodReady(pod *v1.Pod) bool { + return IsPodReadyConditionTrue(pod.Status) +} + +// IsPodReadyConditionTrue returns true if a pod is ready; false otherwise. +func IsPodReadyConditionTrue(status v1.PodStatus) bool { + condition := GetPodReadyCondition(status) + return condition != nil && condition.Status == v1.ConditionTrue +} + +// GetPodReadyCondition extracts the pod ready condition from the given status and returns that. +// Returns nil if the condition is not present. +func GetPodReadyCondition(status v1.PodStatus) *v1.PodCondition { + _, condition := GetPodCondition(&status, v1.PodReady) + return condition +} + +// GetPodCondition extracts the provided condition from the given status and returns that. +// Returns nil and -1 if the condition is not present, and the index of the located condition. +func GetPodCondition(status *v1.PodStatus, conditionType v1.PodConditionType) (int, *v1.PodCondition) { + if status == nil { + return -1, nil + } + return GetPodConditionFromList(status.Conditions, conditionType) +} + +// GetPodConditionFromList extracts the provided condition from the given list of condition and +// returns the index of the condition and the condition. Returns -1 and nil if the condition is not present. +func GetPodConditionFromList(conditions []v1.PodCondition, conditionType v1.PodConditionType) (int, *v1.PodCondition) { + if conditions == nil { + return -1, nil + } + for i := range conditions { + if conditions[i].Type == conditionType { + return i, &conditions[i] + } + } + return -1, nil +} + +// UpdatePodCondition updates existing pod condition or creates a new one. Sets LastTransitionTime to now if the +// status has changed. +// Returns true if pod condition has changed or has been added. +func UpdatePodCondition(status *v1.PodStatus, condition *v1.PodCondition) bool { + condition.LastTransitionTime = metav1.Now() + // Try to find this pod condition. + conditionIndex, oldCondition := GetPodCondition(status, condition.Type) + + if oldCondition == nil { + // We are adding new pod condition. + status.Conditions = append(status.Conditions, *condition) + return true + } + // We are updating an existing condition, so we need to check if it has changed. + if condition.Status == oldCondition.Status { + condition.LastTransitionTime = oldCondition.LastTransitionTime + } + + isEqual := condition.Status == oldCondition.Status && + condition.Reason == oldCondition.Reason && + condition.Message == oldCondition.Message && + condition.LastProbeTime.Equal(&oldCondition.LastProbeTime) && + condition.LastTransitionTime.Equal(&oldCondition.LastTransitionTime) + + status.Conditions[conditionIndex] = *condition + // Return true if one of the fields have changed. + return !isEqual +} + +// GetPodPriority returns priority of the given pod. +func GetPodPriority(pod *v1.Pod) int32 { + if pod.Spec.Priority != nil { + return *pod.Spec.Priority + } + // When priority of a running pod is nil, it means it was created at a time + // that there was no global default priority class and the priority class + // name of the pod was empty. So, we resolve to the static default priority. + return 0 +} diff --git a/kube/pkg/apis/core/helper/helpers.go b/kube/pkg/apis/core/helper/helpers.go new file mode 100644 index 000000000..0c16cb03b --- /dev/null +++ b/kube/pkg/apis/core/helper/helpers.go @@ -0,0 +1,51 @@ +/* + +Copyright 2021 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. + +*/ + +package helper + +import ( + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/conversion" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" +) + +// Semantic can do semantic deep equality checks for core objects. +// Example: apiequality.Semantic.DeepEqual(aPod, aPodWithNonNilButEmptyMaps) == true +var Semantic = conversion.EqualitiesOrDie( + func(a, b resource.Quantity) bool { + // Ignore formatting, only care that numeric value stayed the same. + // TODO: if we decide it's important, it should be safe to start comparing the format. + // + // Uninitialized quantities are equivalent to 0 quantities. + return a.Cmp(b) == 0 + }, + func(a, b metav1.MicroTime) bool { + return a.UTC() == b.UTC() + }, + func(a, b metav1.Time) bool { + return a.UTC() == b.UTC() + }, + func(a, b labels.Selector) bool { + return a.String() == b.String() + }, + func(a, b fields.Selector) bool { + return a.String() == b.String() + }, +) \ No newline at end of file diff --git a/kube/pkg/apis/core/v1/helper/helpers.go b/kube/pkg/apis/core/v1/helper/helpers.go new file mode 100644 index 000000000..02ccaec9b --- /dev/null +++ b/kube/pkg/apis/core/v1/helper/helpers.go @@ -0,0 +1,500 @@ +/* +Copyright 2014 The Kubernetes 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. +*/ + +package helper + +import ( + "encoding/json" + "fmt" + "strings" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/util/validation" + "kubesphere.io/kubesphere/kube/pkg/apis/core/helper" +) + +// IsExtendedResourceName returns true if: +// 1. the resource name is not in the default namespace; +// 2. resource name does not have "requests." prefix, +// to avoid confusion with the convention in quota +// 3. it satisfies the rules in IsQualifiedName() after converted into quota resource name +func IsExtendedResourceName(name v1.ResourceName) bool { + if IsNativeResource(name) || strings.HasPrefix(string(name), v1.DefaultResourceRequestsPrefix) { + return false + } + // Ensure it satisfies the rules in IsQualifiedName() after converted into quota resource name + nameForQuota := fmt.Sprintf("%s%s", v1.DefaultResourceRequestsPrefix, string(name)) + if errs := validation.IsQualifiedName(string(nameForQuota)); len(errs) != 0 { + return false + } + return true +} + +// IsPrefixedNativeResource returns true if the resource name is in the +// *kubernetes.io/ namespace. +func IsPrefixedNativeResource(name v1.ResourceName) bool { + return strings.Contains(string(name), v1.ResourceDefaultNamespacePrefix) +} + +// IsNativeResource returns true if the resource name is in the +// *kubernetes.io/ namespace. Partially-qualified (unprefixed) names are +// implicitly in the kubernetes.io/ namespace. +func IsNativeResource(name v1.ResourceName) bool { + return !strings.Contains(string(name), "/") || + IsPrefixedNativeResource(name) +} + +// IsHugePageResourceName returns true if the resource name has the huge page +// resource prefix. +func IsHugePageResourceName(name v1.ResourceName) bool { + return strings.HasPrefix(string(name), v1.ResourceHugePagesPrefix) +} + +// HugePageResourceName returns a ResourceName with the canonical hugepage +// prefix prepended for the specified page size. The page size is converted +// to its canonical representation. +func HugePageResourceName(pageSize resource.Quantity) v1.ResourceName { + return v1.ResourceName(fmt.Sprintf("%s%s", v1.ResourceHugePagesPrefix, pageSize.String())) +} + +// HugePageSizeFromResourceName returns the page size for the specified huge page +// resource name. If the specified input is not a valid huge page resource name +// an error is returned. +func HugePageSizeFromResourceName(name v1.ResourceName) (resource.Quantity, error) { + if !IsHugePageResourceName(name) { + return resource.Quantity{}, fmt.Errorf("resource name: %s is an invalid hugepage name", name) + } + pageSize := strings.TrimPrefix(string(name), v1.ResourceHugePagesPrefix) + return resource.ParseQuantity(pageSize) +} + +// IsOvercommitAllowed returns true if the resource is in the default +// namespace and is not hugepages. +func IsOvercommitAllowed(name v1.ResourceName) bool { + return IsNativeResource(name) && + !IsHugePageResourceName(name) +} + +func IsAttachableVolumeResourceName(name v1.ResourceName) bool { + return strings.HasPrefix(string(name), v1.ResourceAttachableVolumesPrefix) +} + +// Extended and Hugepages resources +func IsScalarResourceName(name v1.ResourceName) bool { + return IsExtendedResourceName(name) || IsHugePageResourceName(name) || + IsPrefixedNativeResource(name) || IsAttachableVolumeResourceName(name) +} + +// this function aims to check if the service's ClusterIP is set or not +// the objective is not to perform validation here +func IsServiceIPSet(service *v1.Service) bool { + return service.Spec.ClusterIP != v1.ClusterIPNone && service.Spec.ClusterIP != "" +} + +// TODO: make method on LoadBalancerStatus? +func LoadBalancerStatusEqual(l, r *v1.LoadBalancerStatus) bool { + return ingressSliceEqual(l.Ingress, r.Ingress) +} + +func ingressSliceEqual(lhs, rhs []v1.LoadBalancerIngress) bool { + if len(lhs) != len(rhs) { + return false + } + for i := range lhs { + if !ingressEqual(&lhs[i], &rhs[i]) { + return false + } + } + return true +} + +func ingressEqual(lhs, rhs *v1.LoadBalancerIngress) bool { + if lhs.IP != rhs.IP { + return false + } + if lhs.Hostname != rhs.Hostname { + return false + } + return true +} + +// GetAccessModesAsString returns a string representation of an array of access modes. +// modes, when present, are always in the same order: RWO,ROX,RWX. +func GetAccessModesAsString(modes []v1.PersistentVolumeAccessMode) string { + modes = removeDuplicateAccessModes(modes) + modesStr := []string{} + if containsAccessMode(modes, v1.ReadWriteOnce) { + modesStr = append(modesStr, "RWO") + } + if containsAccessMode(modes, v1.ReadOnlyMany) { + modesStr = append(modesStr, "ROX") + } + if containsAccessMode(modes, v1.ReadWriteMany) { + modesStr = append(modesStr, "RWX") + } + return strings.Join(modesStr, ",") +} + +// GetAccessModesAsString returns an array of AccessModes from a string created by GetAccessModesAsString +func GetAccessModesFromString(modes string) []v1.PersistentVolumeAccessMode { + strmodes := strings.Split(modes, ",") + accessModes := []v1.PersistentVolumeAccessMode{} + for _, s := range strmodes { + s = strings.Trim(s, " ") + switch { + case s == "RWO": + accessModes = append(accessModes, v1.ReadWriteOnce) + case s == "ROX": + accessModes = append(accessModes, v1.ReadOnlyMany) + case s == "RWX": + accessModes = append(accessModes, v1.ReadWriteMany) + } + } + return accessModes +} + +// removeDuplicateAccessModes returns an array of access modes without any duplicates +func removeDuplicateAccessModes(modes []v1.PersistentVolumeAccessMode) []v1.PersistentVolumeAccessMode { + accessModes := []v1.PersistentVolumeAccessMode{} + for _, m := range modes { + if !containsAccessMode(accessModes, m) { + accessModes = append(accessModes, m) + } + } + return accessModes +} + +func containsAccessMode(modes []v1.PersistentVolumeAccessMode, mode v1.PersistentVolumeAccessMode) bool { + for _, m := range modes { + if m == mode { + return true + } + } + return false +} + +// NodeSelectorRequirementsAsSelector converts the []NodeSelectorRequirement api type into a struct that implements +// labels.Selector. +func NodeSelectorRequirementsAsSelector(nsm []v1.NodeSelectorRequirement) (labels.Selector, error) { + if len(nsm) == 0 { + return labels.Nothing(), nil + } + selector := labels.NewSelector() + for _, expr := range nsm { + var op selection.Operator + switch expr.Operator { + case v1.NodeSelectorOpIn: + op = selection.In + case v1.NodeSelectorOpNotIn: + op = selection.NotIn + case v1.NodeSelectorOpExists: + op = selection.Exists + case v1.NodeSelectorOpDoesNotExist: + op = selection.DoesNotExist + case v1.NodeSelectorOpGt: + op = selection.GreaterThan + case v1.NodeSelectorOpLt: + op = selection.LessThan + default: + return nil, fmt.Errorf("%q is not a valid node selector operator", expr.Operator) + } + r, err := labels.NewRequirement(expr.Key, op, expr.Values) + if err != nil { + return nil, err + } + selector = selector.Add(*r) + } + return selector, nil +} + +// NodeSelectorRequirementsAsFieldSelector converts the []NodeSelectorRequirement core type into a struct that implements +// fields.Selector. +func NodeSelectorRequirementsAsFieldSelector(nsm []v1.NodeSelectorRequirement) (fields.Selector, error) { + if len(nsm) == 0 { + return fields.Nothing(), nil + } + + selectors := []fields.Selector{} + for _, expr := range nsm { + switch expr.Operator { + case v1.NodeSelectorOpIn: + if len(expr.Values) != 1 { + return nil, fmt.Errorf("unexpected number of value (%d) for node field selector operator %q", + len(expr.Values), expr.Operator) + } + selectors = append(selectors, fields.OneTermEqualSelector(expr.Key, expr.Values[0])) + + case v1.NodeSelectorOpNotIn: + if len(expr.Values) != 1 { + return nil, fmt.Errorf("unexpected number of value (%d) for node field selector operator %q", + len(expr.Values), expr.Operator) + } + selectors = append(selectors, fields.OneTermNotEqualSelector(expr.Key, expr.Values[0])) + + default: + return nil, fmt.Errorf("%q is not a valid node field selector operator", expr.Operator) + } + } + + return fields.AndSelectors(selectors...), nil +} + +// NodeSelectorRequirementKeysExistInNodeSelectorTerms checks if a NodeSelectorTerm with key is already specified in terms +func NodeSelectorRequirementKeysExistInNodeSelectorTerms(reqs []v1.NodeSelectorRequirement, terms []v1.NodeSelectorTerm) bool { + for _, req := range reqs { + for _, term := range terms { + for _, r := range term.MatchExpressions { + if r.Key == req.Key { + return true + } + } + } + } + return false +} + +// MatchNodeSelectorTerms checks whether the node labels and fields match node selector terms in ORed; +// nil or empty term matches no objects. +func MatchNodeSelectorTerms( + nodeSelectorTerms []v1.NodeSelectorTerm, + nodeLabels labels.Set, + nodeFields fields.Set, +) bool { + for _, req := range nodeSelectorTerms { + // nil or empty term selects no objects + if len(req.MatchExpressions) == 0 && len(req.MatchFields) == 0 { + continue + } + + if len(req.MatchExpressions) != 0 { + labelSelector, err := NodeSelectorRequirementsAsSelector(req.MatchExpressions) + if err != nil || !labelSelector.Matches(nodeLabels) { + continue + } + } + + if len(req.MatchFields) != 0 { + fieldSelector, err := NodeSelectorRequirementsAsFieldSelector(req.MatchFields) + if err != nil || !fieldSelector.Matches(nodeFields) { + continue + } + } + + return true + } + + return false +} + +// TopologySelectorRequirementsAsSelector converts the []TopologySelectorLabelRequirement api type into a struct +// that implements labels.Selector. +func TopologySelectorRequirementsAsSelector(tsm []v1.TopologySelectorLabelRequirement) (labels.Selector, error) { + if len(tsm) == 0 { + return labels.Nothing(), nil + } + + selector := labels.NewSelector() + for _, expr := range tsm { + r, err := labels.NewRequirement(expr.Key, selection.In, expr.Values) + if err != nil { + return nil, err + } + selector = selector.Add(*r) + } + + return selector, nil +} + +// MatchTopologySelectorTerms checks whether given labels match topology selector terms in ORed; +// nil or empty term matches no objects; while empty term list matches all objects. +func MatchTopologySelectorTerms(topologySelectorTerms []v1.TopologySelectorTerm, lbls labels.Set) bool { + if len(topologySelectorTerms) == 0 { + // empty term list matches all objects + return true + } + + for _, req := range topologySelectorTerms { + // nil or empty term selects no objects + if len(req.MatchLabelExpressions) == 0 { + continue + } + + labelSelector, err := TopologySelectorRequirementsAsSelector(req.MatchLabelExpressions) + if err != nil || !labelSelector.Matches(lbls) { + continue + } + + return true + } + + return false +} + +// AddOrUpdateTolerationInPodSpec tries to add a toleration to the toleration list in PodSpec. +// Returns true if something was updated, false otherwise. +func AddOrUpdateTolerationInPodSpec(spec *v1.PodSpec, toleration *v1.Toleration) bool { + podTolerations := spec.Tolerations + + var newTolerations []v1.Toleration + updated := false + for i := range podTolerations { + if toleration.MatchToleration(&podTolerations[i]) { + if helper.Semantic.DeepEqual(toleration, podTolerations[i]) { + return false + } + newTolerations = append(newTolerations, *toleration) + updated = true + continue + } + + newTolerations = append(newTolerations, podTolerations[i]) + } + + if !updated { + newTolerations = append(newTolerations, *toleration) + } + + spec.Tolerations = newTolerations + return true +} + +// AddOrUpdateTolerationInPod tries to add a toleration to the pod's toleration list. +// Returns true if something was updated, false otherwise. +func AddOrUpdateTolerationInPod(pod *v1.Pod, toleration *v1.Toleration) bool { + return AddOrUpdateTolerationInPodSpec(&pod.Spec, toleration) +} + +// TolerationsTolerateTaint checks if taint is tolerated by any of the tolerations. +func TolerationsTolerateTaint(tolerations []v1.Toleration, taint *v1.Taint) bool { + for i := range tolerations { + if tolerations[i].ToleratesTaint(taint) { + return true + } + } + return false +} + +type taintsFilterFunc func(*v1.Taint) bool + +// TolerationsTolerateTaintsWithFilter checks if given tolerations tolerates +// all the taints that apply to the filter in given taint list. +func TolerationsTolerateTaintsWithFilter(tolerations []v1.Toleration, taints []v1.Taint, applyFilter taintsFilterFunc) bool { + if len(taints) == 0 { + return true + } + + for i := range taints { + if applyFilter != nil && !applyFilter(&taints[i]) { + continue + } + + if !TolerationsTolerateTaint(tolerations, &taints[i]) { + return false + } + } + + return true +} + +// Returns true and list of Tolerations matching all Taints if all are tolerated, or false otherwise. +func GetMatchingTolerations(taints []v1.Taint, tolerations []v1.Toleration) (bool, []v1.Toleration) { + if len(taints) == 0 { + return true, []v1.Toleration{} + } + if len(tolerations) == 0 && len(taints) > 0 { + return false, []v1.Toleration{} + } + result := []v1.Toleration{} + for i := range taints { + tolerated := false + for j := range tolerations { + if tolerations[j].ToleratesTaint(&taints[i]) { + result = append(result, tolerations[j]) + tolerated = true + break + } + } + if !tolerated { + return false, []v1.Toleration{} + } + } + return true, result +} + +func GetAvoidPodsFromNodeAnnotations(annotations map[string]string) (v1.AvoidPods, error) { + var avoidPods v1.AvoidPods + if len(annotations) > 0 && annotations[v1.PreferAvoidPodsAnnotationKey] != "" { + err := json.Unmarshal([]byte(annotations[v1.PreferAvoidPodsAnnotationKey]), &avoidPods) + if err != nil { + return avoidPods, err + } + } + return avoidPods, nil +} + +// GetPersistentVolumeClass returns StorageClassName. +func GetPersistentVolumeClass(volume *v1.PersistentVolume) string { + // Use beta annotation first + if class, found := volume.Annotations[v1.BetaStorageClassAnnotation]; found { + return class + } + + return volume.Spec.StorageClassName +} + +// GetPersistentVolumeClaimClass returns StorageClassName. If no storage class was +// requested, it returns "". +func GetPersistentVolumeClaimClass(claim *v1.PersistentVolumeClaim) string { + // Use beta annotation first + if class, found := claim.Annotations[v1.BetaStorageClassAnnotation]; found { + return class + } + + if claim.Spec.StorageClassName != nil { + return *claim.Spec.StorageClassName + } + + return "" +} + +// ScopedResourceSelectorRequirementsAsSelector converts the ScopedResourceSelectorRequirement api type into a struct that implements +// labels.Selector. +func ScopedResourceSelectorRequirementsAsSelector(ssr v1.ScopedResourceSelectorRequirement) (labels.Selector, error) { + selector := labels.NewSelector() + var op selection.Operator + switch ssr.Operator { + case v1.ScopeSelectorOpIn: + op = selection.In + case v1.ScopeSelectorOpNotIn: + op = selection.NotIn + case v1.ScopeSelectorOpExists: + op = selection.Exists + case v1.ScopeSelectorOpDoesNotExist: + op = selection.DoesNotExist + default: + return nil, fmt.Errorf("%q is not a valid scope selector operator", ssr.Operator) + } + r, err := labels.NewRequirement(string(ssr.ScopeName), op, ssr.Values) + if err != nil { + return nil, err + } + selector = selector.Add(*r) + return selector, nil +} diff --git a/kube/pkg/apis/core/v1/helper/qos/qos.go b/kube/pkg/apis/core/v1/helper/qos/qos.go new file mode 100644 index 000000000..3779f7d5a --- /dev/null +++ b/kube/pkg/apis/core/v1/helper/qos/qos.go @@ -0,0 +1,103 @@ +/* + + Copyright 2021 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. + +*/ + +package qos + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/util/sets" +) + +var supportedQoSComputeResources = sets.NewString(string(corev1.ResourceCPU), string(corev1.ResourceMemory)) + +// QOSList is a set of (resource name, QoS class) pairs. +type QOSList map[corev1.ResourceName]corev1.PodQOSClass + +func isSupportedQoSComputeResource(name corev1.ResourceName) bool { + return supportedQoSComputeResources.Has(string(name)) +} + +// GetPodQOS returns the QoS class of a pod. +// A pod is besteffort if none of its containers have specified any requests or limits. +// A pod is guaranteed only when requests and limits are specified for all the containers and they are equal. +// A pod is burstable if limits and requests do not match across all containers. +func GetPodQOS(pod *corev1.Pod) corev1.PodQOSClass { + requests := corev1.ResourceList{} + limits := corev1.ResourceList{} + zeroQuantity := resource.MustParse("0") + isGuaranteed := true + allContainers := []corev1.Container{} + allContainers = append(allContainers, pod.Spec.Containers...) + allContainers = append(allContainers, pod.Spec.InitContainers...) + for _, container := range allContainers { + // process requests + for name, quantity := range container.Resources.Requests { + if !isSupportedQoSComputeResource(name) { + continue + } + if quantity.Cmp(zeroQuantity) == 1 { + delta := quantity.DeepCopy() + if _, exists := requests[name]; !exists { + requests[name] = delta + } else { + delta.Add(requests[name]) + requests[name] = delta + } + } + } + // process limits + qosLimitsFound := sets.NewString() + for name, quantity := range container.Resources.Limits { + if !isSupportedQoSComputeResource(name) { + continue + } + if quantity.Cmp(zeroQuantity) == 1 { + qosLimitsFound.Insert(string(name)) + delta := quantity.DeepCopy() + if _, exists := limits[name]; !exists { + limits[name] = delta + } else { + delta.Add(limits[name]) + limits[name] = delta + } + } + } + + if !qosLimitsFound.HasAll(string(corev1.ResourceMemory), string(corev1.ResourceCPU)) { + isGuaranteed = false + } + } + if len(requests) == 0 && len(limits) == 0 { + return corev1.PodQOSBestEffort + } + // Check is requests match limits for all resources. + if isGuaranteed { + for name, req := range requests { + if lim, exists := limits[name]; !exists || lim.Cmp(req) != 0 { + isGuaranteed = false + break + } + } + } + if isGuaranteed && + len(requests) == len(limits) { + return corev1.PodQOSGuaranteed + } + return corev1.PodQOSBurstable +} diff --git a/kube/pkg/features/features.go b/kube/pkg/features/features.go new file mode 100644 index 000000000..cede7dd54 --- /dev/null +++ b/kube/pkg/features/features.go @@ -0,0 +1,680 @@ +/* +Copyright 2017 The Kubernetes 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. +*/ + +package features + +import ( + "k8s.io/component-base/featuregate" +) + +const ( + // Every feature gate should add method here following this template: + // + // // owner: @username + // // alpha: v1.X + // MyFeature featuregate.Feature = "MyFeature" + + // owner: @tallclair + // beta: v1.4 + AppArmor featuregate.Feature = "AppArmor" + + // owner: @mtaufen + // alpha: v1.4 + // beta: v1.11 + DynamicKubeletConfig featuregate.Feature = "DynamicKubeletConfig" + + // owner: @pweil- + // alpha: v1.5 + // + // Default userns=host for containers that are using other host namespaces, host mounts, the pod + // contains a privileged container, or specific non-namespaced capabilities (MKNOD, SYS_MODULE, + // SYS_TIME). This should only be enabled if user namespace remapping is enabled in the docker daemon. + ExperimentalHostUserNamespaceDefaultingGate featuregate.Feature = "ExperimentalHostUserNamespaceDefaulting" + + // owner: @jiayingz + // beta: v1.10 + // + // Enables support for Device Plugins + DevicePlugins featuregate.Feature = "DevicePlugins" + + // owner: @dxist + // alpha: v1.16 + // + // Enables support of HPA scaling to zero pods when an object or custom metric is configured. + HPAScaleToZero featuregate.Feature = "HPAScaleToZero" + + // owner: @mikedanese + // alpha: v1.7 + // beta: v1.12 + // + // Gets a server certificate for the kubelet from the Certificate Signing + // Request API instead of generating one self signed and auto rotates the + // certificate as expiration approaches. + RotateKubeletServerCertificate featuregate.Feature = "RotateKubeletServerCertificate" + + // owner: @jinxu + // beta: v1.10 + // + // New local storage types to support local storage capacity isolation + LocalStorageCapacityIsolation featuregate.Feature = "LocalStorageCapacityIsolation" + + // owner: @gnufied + // beta: v1.11 + // Ability to Expand persistent volumes + ExpandPersistentVolumes featuregate.Feature = "ExpandPersistentVolumes" + + // owner: @mlmhl + // beta: v1.15 + // Ability to expand persistent volumes' file system without unmounting volumes. + ExpandInUsePersistentVolumes featuregate.Feature = "ExpandInUsePersistentVolumes" + + // owner: @gnufied + // alpha: v1.14 + // beta: v1.16 + // Ability to expand CSI volumes + ExpandCSIVolumes featuregate.Feature = "ExpandCSIVolumes" + + // owner: @verb + // alpha: v1.16 + // + // Allows running an ephemeral container in pod namespaces to troubleshoot a running pod. + EphemeralContainers featuregate.Feature = "EphemeralContainers" + + // owner: @sjenning + // alpha: v1.11 + // + // Allows resource reservations at the QoS level preventing pods at lower QoS levels from + // bursting into resources requested at higher QoS levels (memory only for now) + QOSReserved featuregate.Feature = "QOSReserved" + + // owner: @ConnorDoyle + // alpha: v1.8 + // beta: v1.10 + // + // Alternative container-level CPU affinity policies. + CPUManager featuregate.Feature = "CPUManager" + + // owner: @szuecs + // alpha: v1.12 + // + // Enable nodes to change CPUCFSQuotaPeriod + CPUCFSQuotaPeriod featuregate.Feature = "CustomCPUCFSQuotaPeriod" + + // owner: @lmdaly + // alpha: v1.16 + // beta: v1.18 + // + // Enable resource managers to make NUMA aligned decisions + TopologyManager featuregate.Feature = "TopologyManager" + + // owner: @sjenning + // beta: v1.11 + // + // Enable pods to set sysctls on a pod + Sysctls featuregate.Feature = "Sysctls" + + // owner @smarterclayton + // alpha: v1.16 + // beta: v1.19 + // ga: v1.21 + // + // Enable legacy behavior to vary cluster functionality on the node-role.kubernetes.io labels. On by default (legacy), will be turned off in 1.18. + // Lock to false in v1.21 and remove in v1.22. + LegacyNodeRoleBehavior featuregate.Feature = "LegacyNodeRoleBehavior" + + // owner @brendandburns + // alpha: v1.9 + // beta: v1.19 + // ga: v1.21 + // + // Enable nodes to exclude themselves from service load balancers + ServiceNodeExclusion featuregate.Feature = "ServiceNodeExclusion" + + // owner @smarterclayton + // alpha: v1.16 + // beta: v1.19 + // ga: v1.21 + // + // Enable nodes to exclude themselves from network disruption checks + NodeDisruptionExclusion featuregate.Feature = "NodeDisruptionExclusion" + + // owner: @saad-ali + // alpha: v1.12 + // beta: v1.14 + // GA: v1.18 + // Enable all logic related to the CSIDriver API object in storage.k8s.io + CSIDriverRegistry featuregate.Feature = "CSIDriverRegistry" + + // owner: @screeley44 + // alpha: v1.9 + // beta: v1.13 + // ga: v1.18 + // + // Enable Block volume support in containers. + BlockVolume featuregate.Feature = "BlockVolume" + + // owner: @pospispa + // GA: v1.11 + // + // Postpone deletion of a PV or a PVC when they are being used + StorageObjectInUseProtection featuregate.Feature = "StorageObjectInUseProtection" + + // owner: @dims, @derekwaynecarr + // alpha: v1.10 + // beta: v1.14 + // GA: v1.20 + // + // Implement support for limiting pids in pods + SupportPodPidsLimit featuregate.Feature = "SupportPodPidsLimit" + + // owner: @mikedanese + // alpha: v1.13 + // + // Migrate ServiceAccount volumes to use a projected volume consisting of a + // ServiceAccountTokenVolumeProjection. This feature adds new required flags + // to the API server. + BoundServiceAccountTokenVolume featuregate.Feature = "BoundServiceAccountTokenVolume" + + // owner: @mtaufen + // alpha: v1.18 + // beta: v1.20 + // + // Enable OIDC discovery endpoints (issuer and JWKS URLs) for the service + // account issuer in the API server. + // Note these endpoints serve minimally-compliant discovery docs that are + // intended to be used for service account token verification. + ServiceAccountIssuerDiscovery featuregate.Feature = "ServiceAccountIssuerDiscovery" + + // owner: @Random-Liu + // beta: v1.11 + // + // Enable container log rotation for cri container runtime + CRIContainerLogRotation featuregate.Feature = "CRIContainerLogRotation" + + // owner: @krmayankk + // beta: v1.14 + // + // Enables control over the primary group ID of containers' init processes. + RunAsGroup featuregate.Feature = "RunAsGroup" + + // owner: @saad-ali + // ga + // + // Allow mounting a subpath of a volume in a container + // Do not remove this feature gate even though it's GA + VolumeSubpath featuregate.Feature = "VolumeSubpath" + + // owner: @ravig + // alpha: v1.11 + // + // Include volume count on node to be considered for balanced resource allocation while scheduling. + // A node which has closer cpu,memory utilization and volume count is favoured by scheduler + // while making decisions. + BalanceAttachedNodeVolumes featuregate.Feature = "BalanceAttachedNodeVolumes" + + // owner: @vladimirvivien + // alpha: v1.11 + // beta: v1.14 + // ga: v1.18 + // + // Enables CSI to use raw block storage volumes + CSIBlockVolume featuregate.Feature = "CSIBlockVolume" + + // owner: @pohly + // alpha: v1.14 + // beta: v1.16 + // + // Enables CSI Inline volumes support for pods + CSIInlineVolume featuregate.Feature = "CSIInlineVolume" + + // owner: @pohly + // alpha: v1.19 + // + // Enables tracking of available storage capacity that CSI drivers provide. + CSIStorageCapacity featuregate.Feature = "CSIStorageCapacity" + + // owner: @alculquicondor + // beta: v1.20 + // + // Enables the use of PodTopologySpread scheduling plugin to do default + // spreading and disables legacy SelectorSpread plugin. + DefaultPodTopologySpread featuregate.Feature = "DefaultPodTopologySpread" + + // owner: @pohly + // alpha: v1.19 + // + // Enables generic ephemeral inline volume support for pods + GenericEphemeralVolume featuregate.Feature = "GenericEphemeralVolume" + + // owner: @chendave + // alpha: v1.21 + // + // PreferNominatedNode tells scheduler whether the nominated node will be checked first before looping + // all the rest of nodes in the cluster. + // Enabling this feature also implies the preemptor pod might not be dispatched to the best candidate in + // some corner case, e.g. another node releases enough resources after the nominated node has been set + // and hence is the best candidate instead. + PreferNominatedNode featuregate.Feature = "PreferNominatedNode" + + // owner: @tallclair + // alpha: v1.12 + // beta: v1.14 + // GA: v1.20 + // + // Enables RuntimeClass, for selecting between multiple runtimes to run a pod. + RuntimeClass featuregate.Feature = "RuntimeClass" + + // owner: @mtaufen + // alpha: v1.12 + // beta: v1.14 + // GA: v1.17 + // + // Kubelet uses the new Lease API to report node heartbeats, + // (Kube) Node Lifecycle Controller uses these heartbeats as a node health signal. + NodeLease featuregate.Feature = "NodeLease" + + // owner: @janosi + // alpha: v1.12 + // beta: v1.18 + // GA: v1.20 + // + // Enables SCTP as new protocol for Service ports, NetworkPolicy, and ContainerPort in Pod/Containers definition + SCTPSupport featuregate.Feature = "SCTPSupport" + + // owner: @xing-yang + // alpha: v1.12 + // beta: v1.17 + // GA: v1.20 + // + // Enable volume snapshot data source support. + VolumeSnapshotDataSource featuregate.Feature = "VolumeSnapshotDataSource" + + // owner: @jessfraz + // alpha: v1.12 + // + // Enables control over ProcMountType for containers. + ProcMountType featuregate.Feature = "ProcMountType" + + // owner: @janetkuo + // alpha: v1.12 + // + // Allow TTL controller to clean up Pods and Jobs after they finish. + TTLAfterFinished featuregate.Feature = "TTLAfterFinished" + + // owner: @dashpole + // alpha: v1.13 + // beta: v1.15 + // + // Enables the kubelet's pod resources grpc endpoint + KubeletPodResources featuregate.Feature = "KubeletPodResources" + + // owner: @davidz627 + // alpha: v1.14 + // beta: v1.17 + // + // Enables the in-tree storage to CSI Plugin migration feature. + CSIMigration featuregate.Feature = "CSIMigration" + + // owner: @davidz627 + // alpha: v1.14 + // beta: v1.17 + // + // Enables the GCE PD in-tree driver to GCE CSI Driver migration feature. + CSIMigrationGCE featuregate.Feature = "CSIMigrationGCE" + + // owner: @davidz627 + // alpha: v1.17 + // + // Disables the GCE PD in-tree driver. + // Expects GCE PD CSI Driver to be installed and configured on all nodes. + CSIMigrationGCEComplete featuregate.Feature = "CSIMigrationGCEComplete" + + // owner: @leakingtapan + // alpha: v1.14 + // beta: v1.17 + // + // Enables the AWS EBS in-tree driver to AWS EBS CSI Driver migration feature. + CSIMigrationAWS featuregate.Feature = "CSIMigrationAWS" + + // owner: @leakingtapan + // alpha: v1.17 + // + // Disables the AWS EBS in-tree driver. + // Expects AWS EBS CSI Driver to be installed and configured on all nodes. + CSIMigrationAWSComplete featuregate.Feature = "CSIMigrationAWSComplete" + + // owner: @andyzhangx + // alpha: v1.15 + // beta: v1.19 + // + // Enables the Azure Disk in-tree driver to Azure Disk Driver migration feature. + CSIMigrationAzureDisk featuregate.Feature = "CSIMigrationAzureDisk" + + // owner: @andyzhangx + // alpha: v1.17 + // + // Disables the Azure Disk in-tree driver. + // Expects Azure Disk CSI Driver to be installed and configured on all nodes. + CSIMigrationAzureDiskComplete featuregate.Feature = "CSIMigrationAzureDiskComplete" + + // owner: @andyzhangx + // alpha: v1.15 + // + // Enables the Azure File in-tree driver to Azure File Driver migration feature. + CSIMigrationAzureFile featuregate.Feature = "CSIMigrationAzureFile" + + // owner: @andyzhangx + // alpha: v1.17 + // + // Disables the Azure File in-tree driver. + // Expects Azure File CSI Driver to be installed and configured on all nodes. + CSIMigrationAzureFileComplete featuregate.Feature = "CSIMigrationAzureFileComplete" + + // owner: @divyenpatel + // beta: v1.19 (requires: vSphere vCenter/ESXi Version: 7.0u1, HW Version: VM version 15) + // + // Enables the vSphere in-tree driver to vSphere CSI Driver migration feature. + CSIMigrationvSphere featuregate.Feature = "CSIMigrationvSphere" + + // owner: @divyenpatel + // beta: v1.19 (requires: vSphere vCenter/ESXi Version: 7.0u1, HW Version: VM version 15) + // + // Disables the vSphere in-tree driver. + // Expects vSphere CSI Driver to be installed and configured on all nodes. + CSIMigrationvSphereComplete featuregate.Feature = "CSIMigrationvSphereComplete" + + // owner: @huffmanca + // alpha: v1.19 + // beta: v1.20 + // + // Determines if a CSI Driver supports applying fsGroup. + CSIVolumeFSGroupPolicy featuregate.Feature = "CSIVolumeFSGroupPolicy" + + // owner: @gnufied + // alpha: v1.18 + // beta: v1.20 + // Allows user to configure volume permission change policy for fsGroups when mounting + // a volume in a Pod. + ConfigurableFSGroupPolicy featuregate.Feature = "ConfigurableFSGroupPolicy" + + // owner: @RobertKrawitz, @derekwaynecarr + // beta: v1.15 + // GA: v1.20 + // + // Implement support for limiting pids in nodes + SupportNodePidsLimit featuregate.Feature = "SupportNodePidsLimit" + + // owner: @wk8 + // alpha: v1.14 + // beta: v1.16 + // + // Enables GMSA support for Windows workloads. + WindowsGMSA featuregate.Feature = "WindowsGMSA" + + // owner: @bclau + // alpha: v1.16 + // beta: v1.17 + // GA: v1.18 + // + // Enables support for running container entrypoints as different usernames than their default ones. + WindowsRunAsUserName featuregate.Feature = "WindowsRunAsUserName" + + // owner: @adisky + // alpha: v1.14 + // beta: v1.18 + // + // Enables the OpenStack Cinder in-tree driver to OpenStack Cinder CSI Driver migration feature. + CSIMigrationOpenStack featuregate.Feature = "CSIMigrationOpenStack" + + // owner: @adisky + // alpha: v1.17 + // + // Disables the OpenStack Cinder in-tree driver. + // Expects the OpenStack Cinder CSI Driver to be installed and configured on all nodes. + CSIMigrationOpenStackComplete featuregate.Feature = "CSIMigrationOpenStackComplete" + + // owner: @RobertKrawitz + // alpha: v1.15 + // + // Allow use of filesystems for ephemeral storage monitoring. + // Only applies if LocalStorageCapacityIsolation is set. + LocalStorageCapacityIsolationFSQuotaMonitoring featuregate.Feature = "LocalStorageCapacityIsolationFSQuotaMonitoring" + + // owner: @denkensk + // alpha: v1.15 + // beta: v1.19 + // + // Enables NonPreempting option for priorityClass and pod. + NonPreemptingPriority featuregate.Feature = "NonPreemptingPriority" + + // owner: @egernst + // alpha: v1.16 + // beta: v1.18 + // + // Enables PodOverhead, for accounting pod overheads which are specific to a given RuntimeClass + PodOverhead featuregate.Feature = "PodOverhead" + + // owner: @khenidak + // alpha: v1.15 + // + // Enables ipv6 dual stack + IPv6DualStack featuregate.Feature = "IPv6DualStack" + + // owner: @robscott @freehan + // alpha: v1.16 + // + // Enable Endpoint Slices for more scalable Service endpoints. + EndpointSlice featuregate.Feature = "EndpointSlice" + + // owner: @robscott @freehan + // alpha: v1.18 + // beta: v1.19 + // + // Enable Endpoint Slice consumption by kube-proxy for improved scalability. + EndpointSliceProxying featuregate.Feature = "EndpointSliceProxying" + + // owner: @robscott @kumarvin123 + // alpha: v1.19 + // + // Enable Endpoint Slice consumption by kube-proxy in Windows for improved scalability. + WindowsEndpointSliceProxying featuregate.Feature = "WindowsEndpointSliceProxying" + + // owner: @matthyx + // alpha: v1.16 + // beta: v1.18 + // GA: v1.20 + // + // Enables the startupProbe in kubelet worker. + StartupProbe featuregate.Feature = "StartupProbe" + + // owner: @deads2k + // beta: v1.17 + // + // Enables the users to skip TLS verification of kubelets on pod logs requests + AllowInsecureBackendProxy featuregate.Feature = "AllowInsecureBackendProxy" + + // owner: @mortent + // alpha: v1.3 + // beta: v1.5 + // + // Enable all logic related to the PodDisruptionBudget API object in policy + PodDisruptionBudget featuregate.Feature = "PodDisruptionBudget" + + // owner: @alaypatel07, @soltysh + // alpha: v1.20 + // beta: v1.21 + // + // CronJobControllerV2 controls whether the controller manager starts old cronjob + // controller or new one which is implemented with informers and delaying queue + // + // This feature is deprecated, and will be removed in v1.22. + CronJobControllerV2 featuregate.Feature = "CronJobControllerV2" + + // owner: @smarterclayton + // alpha: v1.21 + // + // DaemonSets allow workloads to maintain availability during update per node + DaemonSetUpdateSurge featuregate.Feature = "DaemonSetUpdateSurge" + + // owner: @m1093782566 + // alpha: v1.17 + // + // Enables topology aware service routing + ServiceTopology featuregate.Feature = "ServiceTopology" + + // owner: @robscott + // alpha: v1.18 + // beta: v1.19 + // ga: v1.20 + // + // Enables AppProtocol field for Services and Endpoints. + ServiceAppProtocol featuregate.Feature = "ServiceAppProtocol" + + // owner: @wojtek-t + // alpha: v1.18 + // beta: v1.19 + // ga: v1.21 + // + // Enables a feature to make secrets and configmaps data immutable. + ImmutableEphemeralVolumes featuregate.Feature = "ImmutableEphemeralVolumes" + + // owner: @bart0sh + // alpha: v1.18 + // beta: v1.19 + // + // Enables usage of HugePages- in a volume medium, + // e.g. emptyDir: + // medium: HugePages-1Gi + HugePageStorageMediumSize featuregate.Feature = "HugePageStorageMediumSize" + + // owner: @derekwaynecarr + // alpha: v1.20 + // + // Enables usage of hugepages- in downward API. + DownwardAPIHugePages featuregate.Feature = "DownwardAPIHugePages" + + // owner: @freehan + // GA: v1.18 + // + // Enable ExternalTrafficPolicy for Service ExternalIPs. + // This is for bug fix #69811 + ExternalPolicyForExternalIP featuregate.Feature = "ExternalPolicyForExternalIP" + + // owner: @bswartz + // alpha: v1.18 + // + // Enables usage of any object for volume data source in PVCs + AnyVolumeDataSource featuregate.Feature = "AnyVolumeDataSource" + + // owner: @javidiaz + // alpha: v1.19 + // beta: v1.20 + // + // Allow setting the Fully Qualified Domain Name (FQDN) in the hostname of a Pod. If a Pod does not + // have FQDN, this feature has no effect. + SetHostnameAsFQDN featuregate.Feature = "SetHostnameAsFQDN" + + // owner: @ksubrmnn + // alpha: v1.14 + // beta: v1.20 + // + // Allows kube-proxy to run in Overlay mode for Windows + WinOverlay featuregate.Feature = "WinOverlay" + + // owner: @ksubrmnn + // alpha: v1.14 + // + // Allows kube-proxy to create DSR loadbalancers for Windows + WinDSR featuregate.Feature = "WinDSR" + + // owner: @RenaudWasTaken @dashpole + // alpha: v1.19 + // beta: v1.20 + // + // Disables Accelerator Metrics Collected by Kubelet + DisableAcceleratorUsageMetrics featuregate.Feature = "DisableAcceleratorUsageMetrics" + + // owner: @arjunrn @mwielgus @josephburnett + // alpha: v1.20 + // + // Add support for the HPA to scale based on metrics from individual containers + // in target pods + HPAContainerMetrics featuregate.Feature = "HPAContainerMetrics" + + // owner: @zshihang + // alpha: v1.13 + // beta: v1.20 + // + // Allows kube-controller-manager to publish kube-root-ca.crt configmap to + // every namespace. This feature is a prerequisite of BoundServiceAccountTokenVolume. + RootCAConfigMap featuregate.Feature = "RootCAConfigMap" + + // owner: @andrewsykim + // alpha: v1.20 + // + // Enable Terminating condition in Endpoint Slices. + EndpointSliceTerminatingCondition featuregate.Feature = "EndpointSliceTerminatingCondition" + + // owner: @robscott + // alpha: v1.20 + // + // Enable NodeName field on Endpoint Slices. + EndpointSliceNodeName featuregate.Feature = "EndpointSliceNodeName" + + // owner: @derekwaynecarr + // alpha: v1.20 + // + // Enables kubelet support to size memory backed volumes + SizeMemoryBackedVolumes featuregate.Feature = "SizeMemoryBackedVolumes" + + // owner: @andrewsykim @SergeyKanzhelev + // GA: v1.20 + // + // Ensure kubelet respects exec probe timeouts. Feature gate exists in-case existing workloads + // may depend on old behavior where exec probe timeouts were ignored. + // Lock to default in v1.21 and remove in v1.22. + ExecProbeTimeout featuregate.Feature = "ExecProbeTimeout" + + // owner: @andrewsykim + // alpha: v1.20 + // + // Enable kubelet exec plugins for image pull credentials. + KubeletCredentialProviders featuregate.Feature = "KubeletCredentialProviders" + + // owner: @zshihang + // alpha: v1.20 + // + // Enable kubelet to pass pod's service account token to NodePublishVolume + // call of CSI driver which is mounting volumes for that pod. + CSIServiceAccountToken featuregate.Feature = "CSIServiceAccountToken" + + // owner: @bobbypage + // alpha: v1.20 + // Adds support for kubelet to detect node shutdown and gracefully terminate pods prior to the node being shutdown. + GracefulNodeShutdown featuregate.Feature = "GracefulNodeShutdown" + + // owner: @andrewsykim @uablrek + // alpha: v1.20 + // + // Allows control if NodePorts shall be created for services with "type: LoadBalancer" by defining the spec.AllocateLoadBalancerNodePorts field (bool) + ServiceLBNodePortControl featuregate.Feature = "ServiceLBNodePortControl" + + // owner: @janosi + // alpha: v1.20 + // + // Enables the usage of different protocols in the same Service with type=LoadBalancer + MixedProtocolLBService featuregate.Feature = "MixedProtocolLBService" +) diff --git a/kube/pkg/quota/v1/evaluator/core/persistent_volume_claims.go b/kube/pkg/quota/v1/evaluator/core/persistent_volume_claims.go new file mode 100644 index 000000000..5ca2ab82d --- /dev/null +++ b/kube/pkg/quota/v1/evaluator/core/persistent_volume_claims.go @@ -0,0 +1,188 @@ +/* +Copyright 2016 The Kubernetes 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. +*/ + +package core + +import ( + "fmt" + "strings" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/admission" + utilfeature "k8s.io/apiserver/pkg/util/feature" + "kubesphere.io/kubesphere/kube/pkg/apis/core/v1/helper" + k8sfeatures "kubesphere.io/kubesphere/kube/pkg/features" + quota "kubesphere.io/kubesphere/kube/pkg/quota/v1" + "kubesphere.io/kubesphere/kube/pkg/quota/v1/generic" +) + +// the name used for object count quota +var pvcObjectCountName = generic.ObjectCountQuotaResourceNameFor(corev1.SchemeGroupVersion.WithResource("persistentvolumeclaims").GroupResource()) + +// pvcResources are the set of static resources managed by quota associated with pvcs. +// for each resource in this list, it may be refined dynamically based on storage class. +var pvcResources = []corev1.ResourceName{ + corev1.ResourcePersistentVolumeClaims, + corev1.ResourceRequestsStorage, +} + +// storageClassSuffix is the suffix to the qualified portion of storage class resource name. +// For example, if you want to quota storage by storage class, you would have a declaration +// that follows .storageclass.storage.k8s.io/. +// For example: +// * gold.storageclass.storage.k8s.io/: 500Gi +// * bronze.storageclass.storage.k8s.io/requests.storage: 500Gi +const storageClassSuffix string = ".storageclass.storage.k8s.io/" + +/* TODO: prune? +// ResourceByStorageClass returns a quota resource name by storage class. +func ResourceByStorageClass(storageClass string, resourceName corev1.ResourceName) corev1.ResourceName { + return corev1.ResourceName(string(storageClass + storageClassSuffix + string(resourceName))) +} +*/ + +// V1ResourceByStorageClass returns a quota resource name by storage class. +func V1ResourceByStorageClass(storageClass string, resourceName corev1.ResourceName) corev1.ResourceName { + return corev1.ResourceName(string(storageClass + storageClassSuffix + string(resourceName))) +} + +// NewPersistentVolumeClaimEvaluator returns an evaluator that can evaluate persistent volume claims +func NewPersistentVolumeClaimEvaluator(f quota.ListerForResourceFunc) quota.Evaluator { + listFuncByNamespace := generic.ListResourceUsingListerFunc(f, corev1.SchemeGroupVersion.WithResource("persistentvolumeclaims")) + pvcEvaluator := &pvcEvaluator{listFuncByNamespace: listFuncByNamespace} + return pvcEvaluator +} + +// pvcEvaluator knows how to evaluate quota usage for persistent volume claims +type pvcEvaluator struct { + // listFuncByNamespace knows how to list pvc claims + listFuncByNamespace generic.ListFuncByNamespace +} + +// Constraints verifies that all required resources are present on the item. +func (p *pvcEvaluator) Constraints(required []corev1.ResourceName, item runtime.Object) error { + // no-op for persistent volume claims + return nil +} + +// GroupResource that this evaluator tracks +func (p *pvcEvaluator) GroupResource() schema.GroupResource { + return corev1.SchemeGroupVersion.WithResource("persistentvolumeclaims").GroupResource() +} + +// Handles returns true if the evaluator should handle the specified operation. +func (p *pvcEvaluator) Handles(a admission.Attributes) bool { + op := a.GetOperation() + if op == admission.Create { + return true + } + if op == admission.Update && utilfeature.DefaultFeatureGate.Enabled(k8sfeatures.ExpandPersistentVolumes) { + return true + } + return false +} + +// Matches returns true if the evaluator matches the specified quota with the provided input item +func (p *pvcEvaluator) Matches(resourceQuota *corev1.ResourceQuota, item runtime.Object) (bool, error) { + return generic.Matches(resourceQuota, item, p.MatchingResources, generic.MatchesNoScopeFunc) +} + +// MatchingScopes takes the input specified list of scopes and input object. Returns the set of scopes resource matches. +func (p *pvcEvaluator) MatchingScopes(item runtime.Object, scopes []corev1.ScopedResourceSelectorRequirement) ([]corev1.ScopedResourceSelectorRequirement, error) { + return []corev1.ScopedResourceSelectorRequirement{}, nil +} + +// UncoveredQuotaScopes takes the input matched scopes which are limited by configuration and the matched quota scopes. +// It returns the scopes which are in limited scopes but dont have a corresponding covering quota scope +func (p *pvcEvaluator) UncoveredQuotaScopes(limitedScopes []corev1.ScopedResourceSelectorRequirement, matchedQuotaScopes []corev1.ScopedResourceSelectorRequirement) ([]corev1.ScopedResourceSelectorRequirement, error) { + return []corev1.ScopedResourceSelectorRequirement{}, nil +} + +// MatchingResources takes the input specified list of resources and returns the set of resources it matches. +func (p *pvcEvaluator) MatchingResources(items []corev1.ResourceName) []corev1.ResourceName { + result := []corev1.ResourceName{} + for _, item := range items { + // match object count quota fields + if quota.Contains([]corev1.ResourceName{pvcObjectCountName}, item) { + result = append(result, item) + continue + } + // match pvc resources + if quota.Contains(pvcResources, item) { + result = append(result, item) + continue + } + // match pvc resources scoped by storage class (.storage-class.kubernetes.io/) + for _, resource := range pvcResources { + byStorageClass := storageClassSuffix + string(resource) + if strings.HasSuffix(string(item), byStorageClass) { + result = append(result, item) + break + } + } + } + return result +} + +// Usage knows how to measure usage associated with item. +func (p *pvcEvaluator) Usage(item runtime.Object) (corev1.ResourceList, error) { + result := corev1.ResourceList{} + pvc, err := toExternalPersistentVolumeClaimOrError(item) + if err != nil { + return result, err + } + + // charge for claim + result[corev1.ResourcePersistentVolumeClaims] = *(resource.NewQuantity(1, resource.DecimalSI)) + result[pvcObjectCountName] = *(resource.NewQuantity(1, resource.DecimalSI)) + storageClassRef := helper.GetPersistentVolumeClaimClass(pvc) + if len(storageClassRef) > 0 { + storageClassClaim := corev1.ResourceName(storageClassRef + storageClassSuffix + string(corev1.ResourcePersistentVolumeClaims)) + result[storageClassClaim] = *(resource.NewQuantity(1, resource.DecimalSI)) + } + + // charge for storage + if request, found := pvc.Spec.Resources.Requests[corev1.ResourceStorage]; found { + result[corev1.ResourceRequestsStorage] = request + // charge usage to the storage class (if present) + if len(storageClassRef) > 0 { + storageClassStorage := corev1.ResourceName(storageClassRef + storageClassSuffix + string(corev1.ResourceRequestsStorage)) + result[storageClassStorage] = request + } + } + return result, nil +} + +// UsageStats calculates aggregate usage for the object. +func (p *pvcEvaluator) UsageStats(options quota.UsageStatsOptions) (quota.UsageStats, error) { + return generic.CalculateUsageStats(options, p.listFuncByNamespace, generic.MatchesNoScopeFunc, p.Usage) +} + +// ensure we implement required interface +var _ quota.Evaluator = &pvcEvaluator{} + +func toExternalPersistentVolumeClaimOrError(obj runtime.Object) (*corev1.PersistentVolumeClaim, error) { + pvc := &corev1.PersistentVolumeClaim{} + switch t := obj.(type) { + case *corev1.PersistentVolumeClaim: + pvc = t + default: + return nil, fmt.Errorf("expect *v1.PersistentVolumeClaim, got %v", t) + } + return pvc, nil +} diff --git a/kube/pkg/quota/v1/evaluator/core/pods.go b/kube/pkg/quota/v1/evaluator/core/pods.go new file mode 100644 index 000000000..2be574741 --- /dev/null +++ b/kube/pkg/quota/v1/evaluator/core/pods.go @@ -0,0 +1,397 @@ +/* +Copyright 2016 The Kubernetes 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. +*/ + +package core + +import ( + "fmt" + "strings" + "time" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + "k8s.io/apimachinery/pkg/util/clock" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apiserver/pkg/admission" + "kubesphere.io/kubesphere/kube/pkg/apis/core/v1/helper" + "kubesphere.io/kubesphere/kube/pkg/apis/core/v1/helper/qos" + quota "kubesphere.io/kubesphere/kube/pkg/quota/v1" + "kubesphere.io/kubesphere/kube/pkg/quota/v1/generic" +) + +// the name used for object count quota +var podObjectCountName = generic.ObjectCountQuotaResourceNameFor(corev1.SchemeGroupVersion.WithResource("pods").GroupResource()) + +// podResources are the set of resources managed by quota associated with pods. +var podResources = []corev1.ResourceName{ + podObjectCountName, + corev1.ResourceCPU, + corev1.ResourceMemory, + corev1.ResourceEphemeralStorage, + corev1.ResourceRequestsCPU, + corev1.ResourceRequestsMemory, + corev1.ResourceRequestsEphemeralStorage, + corev1.ResourceLimitsCPU, + corev1.ResourceLimitsMemory, + corev1.ResourceLimitsEphemeralStorage, + corev1.ResourcePods, +} + +// podResourcePrefixes are the set of prefixes for resources (Hugepages, and other +// potential extended reources with specific prefix) managed by quota associated with pods. +var podResourcePrefixes = []string{ + corev1.ResourceHugePagesPrefix, + corev1.ResourceRequestsHugePagesPrefix, +} + +// requestedResourcePrefixes are the set of prefixes for resources +// that might be declared in pod's Resources.Requests/Limits +var requestedResourcePrefixes = []string{ + corev1.ResourceHugePagesPrefix, +} + +// maskResourceWithPrefix mask resource with certain prefix +// e.g. hugepages-XXX -> requests.hugepages-XXX +func maskResourceWithPrefix(resource corev1.ResourceName, prefix string) corev1.ResourceName { + return corev1.ResourceName(fmt.Sprintf("%s%s", prefix, string(resource))) +} + +// isExtendedResourceNameForQuota returns true if the extended resource name +// has the quota related resource prefix. +func isExtendedResourceNameForQuota(name corev1.ResourceName) bool { + // As overcommit is not supported by extended resources for now, + // only quota objects in format of "requests.resourceName" is allowed. + return !helper.IsNativeResource(name) && strings.HasPrefix(string(name), corev1.DefaultResourceRequestsPrefix) +} + +// NOTE: it was a mistake, but if a quota tracks cpu or memory related resources, +// the incoming pod is required to have those values set. we should not repeat +// this mistake for other future resources (gpus, ephemeral-storage,etc). +// do not add more resources to this list! +var validationSet = sets.NewString( + string(corev1.ResourceCPU), + string(corev1.ResourceMemory), + string(corev1.ResourceRequestsCPU), + string(corev1.ResourceRequestsMemory), + string(corev1.ResourceLimitsCPU), + string(corev1.ResourceLimitsMemory), +) + +// NewPodEvaluator returns an evaluator that can evaluate pods +func NewPodEvaluator(f quota.ListerForResourceFunc, clock clock.Clock) quota.Evaluator { + listFuncByNamespace := generic.ListResourceUsingListerFunc(f, corev1.SchemeGroupVersion.WithResource("pods")) + podEvaluator := &podEvaluator{listFuncByNamespace: listFuncByNamespace, clock: clock} + return podEvaluator +} + +// podEvaluator knows how to measure usage of pods. +type podEvaluator struct { + // knows how to list pods + listFuncByNamespace generic.ListFuncByNamespace + // used to track time + clock clock.Clock +} + +// Constraints verifies that all required resources are present on the pod +// In addition, it validates that the resources are valid (i.e. requests < limits) +func (p *podEvaluator) Constraints(required []corev1.ResourceName, item runtime.Object) error { + pod, err := toExternalPodOrError(item) + if err != nil { + return err + } + + // BACKWARD COMPATIBILITY REQUIREMENT: if we quota cpu or memory, then each container + // must make an explicit request for the resource. this was a mistake. it coupled + // validation with resource counting, but we did this before QoS was even defined. + // let's not make that mistake again with other resources now that QoS is defined. + requiredSet := quota.ToSet(required).Intersection(validationSet) + missingSet := sets.NewString() + for i := range pod.Spec.Containers { + enforcePodContainerConstraints(&pod.Spec.Containers[i], requiredSet, missingSet) + } + for i := range pod.Spec.InitContainers { + enforcePodContainerConstraints(&pod.Spec.InitContainers[i], requiredSet, missingSet) + } + if len(missingSet) == 0 { + return nil + } + return fmt.Errorf("must specify %s", strings.Join(missingSet.List(), ",")) +} + +// GroupResource that this evaluator tracks +func (p *podEvaluator) GroupResource() schema.GroupResource { + return corev1.SchemeGroupVersion.WithResource("pods").GroupResource() +} + +// Handles returns true if the evaluator should handle the specified attributes. +func (p *podEvaluator) Handles(a admission.Attributes) bool { + op := a.GetOperation() + if op == admission.Create { + return true + } + return false +} + +// Matches returns true if the evaluator matches the specified quota with the provided input item +func (p *podEvaluator) Matches(resourceQuota *corev1.ResourceQuota, item runtime.Object) (bool, error) { + return generic.Matches(resourceQuota, item, p.MatchingResources, podMatchesScopeFunc) +} + +// MatchingResources takes the input specified list of resources and returns the set of resources it matches. +func (p *podEvaluator) MatchingResources(input []corev1.ResourceName) []corev1.ResourceName { + result := quota.Intersection(input, podResources) + for _, resource := range input { + // for resources with certain prefix, e.g. hugepages + if quota.ContainsPrefix(podResourcePrefixes, resource) { + result = append(result, resource) + } + // for extended resources + if isExtendedResourceNameForQuota(resource) { + result = append(result, resource) + } + } + + return result +} + +// MatchingScopes takes the input specified list of scopes and pod object. Returns the set of scope selectors pod matches. +func (p *podEvaluator) MatchingScopes(item runtime.Object, scopeSelectors []corev1.ScopedResourceSelectorRequirement) ([]corev1.ScopedResourceSelectorRequirement, error) { + matchedScopes := []corev1.ScopedResourceSelectorRequirement{} + for _, selector := range scopeSelectors { + match, err := podMatchesScopeFunc(selector, item) + if err != nil { + return []corev1.ScopedResourceSelectorRequirement{}, fmt.Errorf("error on matching scope %v: %v", selector, err) + } + if match { + matchedScopes = append(matchedScopes, selector) + } + } + return matchedScopes, nil +} + +// UncoveredQuotaScopes takes the input matched scopes which are limited by configuration and the matched quota scopes. +// It returns the scopes which are in limited scopes but dont have a corresponding covering quota scope +func (p *podEvaluator) UncoveredQuotaScopes(limitedScopes []corev1.ScopedResourceSelectorRequirement, matchedQuotaScopes []corev1.ScopedResourceSelectorRequirement) ([]corev1.ScopedResourceSelectorRequirement, error) { + uncoveredScopes := []corev1.ScopedResourceSelectorRequirement{} + for _, selector := range limitedScopes { + isCovered := false + for _, matchedScopeSelector := range matchedQuotaScopes { + if matchedScopeSelector.ScopeName == selector.ScopeName { + isCovered = true + break + } + } + + if !isCovered { + uncoveredScopes = append(uncoveredScopes, selector) + } + } + return uncoveredScopes, nil +} + +// Usage knows how to measure usage associated with pods +func (p *podEvaluator) Usage(item runtime.Object) (corev1.ResourceList, error) { + // delegate to normal usage + return PodUsageFunc(item, p.clock) +} + +// UsageStats calculates aggregate usage for the object. +func (p *podEvaluator) UsageStats(options quota.UsageStatsOptions) (quota.UsageStats, error) { + return generic.CalculateUsageStats(options, p.listFuncByNamespace, podMatchesScopeFunc, p.Usage) +} + +// verifies we implement the required interface. +var _ quota.Evaluator = &podEvaluator{} + +// enforcePodContainerConstraints checks for required resources that are not set on this container and +// adds them to missingSet. +func enforcePodContainerConstraints(container *corev1.Container, requiredSet, missingSet sets.String) { + requests := container.Resources.Requests + limits := container.Resources.Limits + containerUsage := podComputeUsageHelper(requests, limits) + containerSet := quota.ToSet(quota.ResourceNames(containerUsage)) + if !containerSet.Equal(requiredSet) { + difference := requiredSet.Difference(containerSet) + missingSet.Insert(difference.List()...) + } +} + +// podComputeUsageHelper can summarize the pod compute quota usage based on requests and limits +func podComputeUsageHelper(requests corev1.ResourceList, limits corev1.ResourceList) corev1.ResourceList { + result := corev1.ResourceList{} + result[corev1.ResourcePods] = resource.MustParse("1") + if request, found := requests[corev1.ResourceCPU]; found { + result[corev1.ResourceCPU] = request + result[corev1.ResourceRequestsCPU] = request + } + if limit, found := limits[corev1.ResourceCPU]; found { + result[corev1.ResourceLimitsCPU] = limit + } + if request, found := requests[corev1.ResourceMemory]; found { + result[corev1.ResourceMemory] = request + result[corev1.ResourceRequestsMemory] = request + } + if limit, found := limits[corev1.ResourceMemory]; found { + result[corev1.ResourceLimitsMemory] = limit + } + if request, found := requests[corev1.ResourceEphemeralStorage]; found { + result[corev1.ResourceEphemeralStorage] = request + result[corev1.ResourceRequestsEphemeralStorage] = request + } + if limit, found := limits[corev1.ResourceEphemeralStorage]; found { + result[corev1.ResourceLimitsEphemeralStorage] = limit + } + for resource, request := range requests { + // for resources with certain prefix, e.g. hugepages + if quota.ContainsPrefix(requestedResourcePrefixes, resource) { + result[resource] = request + result[maskResourceWithPrefix(resource, corev1.DefaultResourceRequestsPrefix)] = request + } + // for extended resources + if helper.IsExtendedResourceName(resource) { + // only quota objects in format of "requests.resourceName" is allowed for extended resource. + result[maskResourceWithPrefix(resource, corev1.DefaultResourceRequestsPrefix)] = request + } + } + + return result +} + +func toExternalPodOrError(obj runtime.Object) (*corev1.Pod, error) { + pod := &corev1.Pod{} + switch t := obj.(type) { + case *corev1.Pod: + pod = t + default: + return nil, fmt.Errorf("expect *v1.Pod, got %v", t) + } + return pod, nil +} + +// podMatchesScopeFunc is a function that knows how to evaluate if a pod matches a scope +func podMatchesScopeFunc(selector corev1.ScopedResourceSelectorRequirement, object runtime.Object) (bool, error) { + pod, err := toExternalPodOrError(object) + if err != nil { + return false, err + } + switch selector.ScopeName { + case corev1.ResourceQuotaScopeTerminating: + return isTerminating(pod), nil + case corev1.ResourceQuotaScopeNotTerminating: + return !isTerminating(pod), nil + case corev1.ResourceQuotaScopeBestEffort: + return isBestEffort(pod), nil + case corev1.ResourceQuotaScopeNotBestEffort: + return !isBestEffort(pod), nil + case corev1.ResourceQuotaScopePriorityClass: + return podMatchesSelector(pod, selector) + } + return false, nil +} + +// PodUsageFunc returns the quota usage for a pod. +// A pod is charged for quota if the following are not true. +// - pod has a terminal phase (failed or succeeded) +// - pod has been marked for deletion and grace period has expired +func PodUsageFunc(obj runtime.Object, clock clock.Clock) (corev1.ResourceList, error) { + pod, err := toExternalPodOrError(obj) + if err != nil { + return corev1.ResourceList{}, err + } + + // always quota the object count (even if the pod is end of life) + // object count quotas track all objects that are in storage. + // where "pods" tracks all pods that have not reached a terminal state, + // count/pods tracks all pods independent of state. + result := corev1.ResourceList{ + podObjectCountName: *(resource.NewQuantity(1, resource.DecimalSI)), + } + + // by convention, we do not quota compute resources that have reached end-of life + // note: the "pods" resource is considered a compute resource since it is tied to life-cycle. + if !QuotaV1Pod(pod, clock) { + return result, nil + } + + requests := corev1.ResourceList{} + limits := corev1.ResourceList{} + // TODO: ideally, we have pod level requests and limits in the future. + for i := range pod.Spec.Containers { + requests = quota.Add(requests, pod.Spec.Containers[i].Resources.Requests) + limits = quota.Add(limits, pod.Spec.Containers[i].Resources.Limits) + } + // InitContainers are run sequentially before other containers start, so the highest + // init container resource is compared against the sum of app containers to determine + // the effective usage for both requests and limits. + for i := range pod.Spec.InitContainers { + requests = quota.Max(requests, pod.Spec.InitContainers[i].Resources.Requests) + limits = quota.Max(limits, pod.Spec.InitContainers[i].Resources.Limits) + } + + result = quota.Add(result, podComputeUsageHelper(requests, limits)) + return result, nil +} + +func isBestEffort(pod *corev1.Pod) bool { + return qos.GetPodQOS(pod) == corev1.PodQOSBestEffort +} + +func isTerminating(pod *corev1.Pod) bool { + if pod.Spec.ActiveDeadlineSeconds != nil && *pod.Spec.ActiveDeadlineSeconds >= int64(0) { + return true + } + return false +} + +func podMatchesSelector(pod *corev1.Pod, selector corev1.ScopedResourceSelectorRequirement) (bool, error) { + labelSelector, err := helper.ScopedResourceSelectorRequirementsAsSelector(selector) + if err != nil { + return false, fmt.Errorf("failed to parse and convert selector: %v", err) + } + var m map[string]string + if len(pod.Spec.PriorityClassName) != 0 { + m = map[string]string{string(corev1.ResourceQuotaScopePriorityClass): pod.Spec.PriorityClassName} + } + if labelSelector.Matches(labels.Set(m)) { + return true, nil + } + return false, nil +} + +// QuotaV1Pod returns true if the pod is eligible to track against a quota +// if it's not in a terminal state according to its phase. +func QuotaV1Pod(pod *corev1.Pod, clock clock.Clock) bool { + // if pod is terminal, ignore it for quota + if corev1.PodFailed == pod.Status.Phase || corev1.PodSucceeded == pod.Status.Phase { + return false + } + // if pods are stuck terminating (for example, a node is lost), we do not want + // to charge the user for that pod in quota because it could prevent them from + // scaling up new pods to service their application. + if pod.DeletionTimestamp != nil && pod.DeletionGracePeriodSeconds != nil { + now := clock.Now() + deletionTime := pod.DeletionTimestamp.Time + gracePeriod := time.Duration(*pod.DeletionGracePeriodSeconds) * time.Second + if now.After(deletionTime.Add(gracePeriod)) { + return false + } + } + return true +} diff --git a/kube/pkg/quota/v1/evaluator/core/registry.go b/kube/pkg/quota/v1/evaluator/core/registry.go new file mode 100644 index 000000000..f18cff555 --- /dev/null +++ b/kube/pkg/quota/v1/evaluator/core/registry.go @@ -0,0 +1,49 @@ +/* +Copyright 2016 The Kubernetes 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. +*/ + +package core + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/clock" + quota "kubesphere.io/kubesphere/kube/pkg/quota/v1" + "kubesphere.io/kubesphere/kube/pkg/quota/v1/generic" +) + +// legacyObjectCountAliases are what we used to do simple object counting quota with mapped to alias +var legacyObjectCountAliases = map[schema.GroupVersionResource]corev1.ResourceName{ + corev1.SchemeGroupVersion.WithResource("configmaps"): corev1.ResourceConfigMaps, + corev1.SchemeGroupVersion.WithResource("resourcequotas"): corev1.ResourceQuotas, + corev1.SchemeGroupVersion.WithResource("replicationcontrollers"): corev1.ResourceReplicationControllers, + corev1.SchemeGroupVersion.WithResource("secrets"): corev1.ResourceSecrets, +} + +// NewEvaluators returns the list of static evaluators that manage more than counts +func NewEvaluators(f quota.ListerForResourceFunc) []quota.Evaluator { + // these evaluators have special logic + result := []quota.Evaluator{ + NewPodEvaluator(f, clock.RealClock{}), + NewServiceEvaluator(f), + NewPersistentVolumeClaimEvaluator(f), + } + // these evaluators require an alias for backwards compatibility + for gvr, alias := range legacyObjectCountAliases { + result = append(result, + generic.NewObjectCountEvaluator(gvr.GroupResource(), generic.ListResourceUsingListerFunc(f, gvr), alias)) + } + return result +} diff --git a/kube/pkg/quota/v1/evaluator/core/services.go b/kube/pkg/quota/v1/evaluator/core/services.go new file mode 100644 index 000000000..82517b717 --- /dev/null +++ b/kube/pkg/quota/v1/evaluator/core/services.go @@ -0,0 +1,149 @@ +/* +Copyright 2016 The Kubernetes 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. +*/ + +package core + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/admission" + "kubesphere.io/kubesphere/kube/pkg/quota/v1" + "kubesphere.io/kubesphere/kube/pkg/quota/v1/generic" +) + +// the name used for object count quota +var serviceObjectCountName = generic.ObjectCountQuotaResourceNameFor(corev1.SchemeGroupVersion.WithResource("services").GroupResource()) + +// serviceResources are the set of resources managed by quota associated with services. +var serviceResources = []corev1.ResourceName{ + serviceObjectCountName, + corev1.ResourceServices, + corev1.ResourceServicesNodePorts, + corev1.ResourceServicesLoadBalancers, +} + +// NewServiceEvaluator returns an evaluator that can evaluate services. +func NewServiceEvaluator(f quota.ListerForResourceFunc) quota.Evaluator { + listFuncByNamespace := generic.ListResourceUsingListerFunc(f, corev1.SchemeGroupVersion.WithResource("services")) + serviceEvaluator := &serviceEvaluator{listFuncByNamespace: listFuncByNamespace} + return serviceEvaluator +} + +// serviceEvaluator knows how to measure usage for services. +type serviceEvaluator struct { + // knows how to list items by namespace + listFuncByNamespace generic.ListFuncByNamespace +} + +// Constraints verifies that all required resources are present on the item +func (p *serviceEvaluator) Constraints(required []corev1.ResourceName, item runtime.Object) error { + // this is a no-op for services + return nil +} + +// GroupResource that this evaluator tracks +func (p *serviceEvaluator) GroupResource() schema.GroupResource { + return corev1.SchemeGroupVersion.WithResource("services").GroupResource() +} + +// Handles returns true of the evaluator should handle the specified operation. +func (p *serviceEvaluator) Handles(a admission.Attributes) bool { + operation := a.GetOperation() + // We handle create and update because a service type can change. + return admission.Create == operation || admission.Update == operation +} + +// Matches returns true if the evaluator matches the specified quota with the provided input item +func (p *serviceEvaluator) Matches(resourceQuota *corev1.ResourceQuota, item runtime.Object) (bool, error) { + return generic.Matches(resourceQuota, item, p.MatchingResources, generic.MatchesNoScopeFunc) +} + +// MatchingResources takes the input specified list of resources and returns the set of resources it matches. +func (p *serviceEvaluator) MatchingResources(input []corev1.ResourceName) []corev1.ResourceName { + return quota.Intersection(input, serviceResources) +} + +// MatchingScopes takes the input specified list of scopes and input object. Returns the set of scopes resource matches. +func (p *serviceEvaluator) MatchingScopes(item runtime.Object, scopes []corev1.ScopedResourceSelectorRequirement) ([]corev1.ScopedResourceSelectorRequirement, error) { + return []corev1.ScopedResourceSelectorRequirement{}, nil +} + +// UncoveredQuotaScopes takes the input matched scopes which are limited by configuration and the matched quota scopes. +// It returns the scopes which are in limited scopes but dont have a corresponding covering quota scope +func (p *serviceEvaluator) UncoveredQuotaScopes(limitedScopes []corev1.ScopedResourceSelectorRequirement, matchedQuotaScopes []corev1.ScopedResourceSelectorRequirement) ([]corev1.ScopedResourceSelectorRequirement, error) { + return []corev1.ScopedResourceSelectorRequirement{}, nil +} + +// convert the input object to an internal service object or error. +func toExternalServiceOrError(obj runtime.Object) (*corev1.Service, error) { + svc := &corev1.Service{} + switch t := obj.(type) { + case *corev1.Service: + svc = t + default: + return nil, fmt.Errorf("expect *v1.Service, got %v", t) + } + return svc, nil +} + +// Usage knows how to measure usage associated with services +func (p *serviceEvaluator) Usage(item runtime.Object) (corev1.ResourceList, error) { + result := corev1.ResourceList{} + svc, err := toExternalServiceOrError(item) + if err != nil { + return result, err + } + ports := len(svc.Spec.Ports) + // default service usage + result[serviceObjectCountName] = *(resource.NewQuantity(1, resource.DecimalSI)) + result[corev1.ResourceServices] = *(resource.NewQuantity(1, resource.DecimalSI)) + result[corev1.ResourceServicesLoadBalancers] = resource.Quantity{Format: resource.DecimalSI} + result[corev1.ResourceServicesNodePorts] = resource.Quantity{Format: resource.DecimalSI} + switch svc.Spec.Type { + case corev1.ServiceTypeNodePort: + // node port services need to count node ports + value := resource.NewQuantity(int64(ports), resource.DecimalSI) + result[corev1.ResourceServicesNodePorts] = *value + case corev1.ServiceTypeLoadBalancer: + // load balancer services need to count node ports and load balancers + value := resource.NewQuantity(int64(ports), resource.DecimalSI) + result[corev1.ResourceServicesNodePorts] = *value + result[corev1.ResourceServicesLoadBalancers] = *(resource.NewQuantity(1, resource.DecimalSI)) + } + return result, nil +} + +// UsageStats calculates aggregate usage for the object. +func (p *serviceEvaluator) UsageStats(options quota.UsageStatsOptions) (quota.UsageStats, error) { + return generic.CalculateUsageStats(options, p.listFuncByNamespace, generic.MatchesNoScopeFunc, p.Usage) +} + +var _ quota.Evaluator = &serviceEvaluator{} + +//GetQuotaServiceType returns ServiceType if the service type is eligible to track against a quota, nor return "" +func GetQuotaServiceType(service *corev1.Service) corev1.ServiceType { + switch service.Spec.Type { + case corev1.ServiceTypeNodePort: + return corev1.ServiceTypeNodePort + case corev1.ServiceTypeLoadBalancer: + return corev1.ServiceTypeLoadBalancer + } + return corev1.ServiceType("") +} diff --git a/kube/pkg/quota/v1/generic/configuration.go b/kube/pkg/quota/v1/generic/configuration.go new file mode 100644 index 000000000..fd9355745 --- /dev/null +++ b/kube/pkg/quota/v1/generic/configuration.go @@ -0,0 +1,44 @@ +/* +Copyright 2017 The Kubernetes 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. +*/ + +package generic + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "kubesphere.io/kubesphere/kube/pkg/quota/v1" +) + +// implements a basic configuration +type simpleConfiguration struct { + evaluators []quota.Evaluator + ignoredResources map[schema.GroupResource]struct{} +} + +// NewConfiguration creates a quota configuration +func NewConfiguration(evaluators []quota.Evaluator, ignoredResources map[schema.GroupResource]struct{}) quota.Configuration { + return &simpleConfiguration{ + evaluators: evaluators, + ignoredResources: ignoredResources, + } +} + +func (c *simpleConfiguration) IgnoredResources() map[schema.GroupResource]struct{} { + return c.ignoredResources +} + +func (c *simpleConfiguration) Evaluators() []quota.Evaluator { + return c.evaluators +} diff --git a/kube/pkg/quota/v1/generic/evaluator.go b/kube/pkg/quota/v1/generic/evaluator.go new file mode 100644 index 000000000..cc3f000db --- /dev/null +++ b/kube/pkg/quota/v1/generic/evaluator.go @@ -0,0 +1,319 @@ +/* +Copyright 2016 The Kubernetes 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. +*/ + +package generic + +import ( + "fmt" + "sync/atomic" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/admission" + "k8s.io/client-go/informers" + "k8s.io/client-go/tools/cache" + quota "kubesphere.io/kubesphere/kube/pkg/quota/v1" +) + +// InformerForResourceFunc knows how to provision an informer +type InformerForResourceFunc func(schema.GroupVersionResource) (informers.GenericInformer, error) + +// ListerFuncForResourceFunc knows how to provision a lister from an informer func. +// The lister returns errors until the informer has synced. +func ListerFuncForResourceFunc(f InformerForResourceFunc) quota.ListerForResourceFunc { + return func(gvr schema.GroupVersionResource) (cache.GenericLister, error) { + informer, err := f(gvr) + if err != nil { + return nil, err + } + return &protectedLister{ + hasSynced: cachedHasSynced(informer.Informer().HasSynced), + notReadyErr: fmt.Errorf("%v not yet synced", gvr), + delegate: informer.Lister(), + }, nil + } +} + +// cachedHasSynced returns a function that calls hasSynced() until it returns true once, then returns true +func cachedHasSynced(hasSynced func() bool) func() bool { + cache := &atomic.Value{} + cache.Store(false) + return func() bool { + if cache.Load().(bool) { + // short-circuit if already synced + return true + } + if hasSynced() { + // remember we synced + cache.Store(true) + return true + } + return false + } +} + +// protectedLister returns notReadyError if hasSynced returns false, otherwise delegates to delegate +type protectedLister struct { + hasSynced func() bool + notReadyErr error + delegate cache.GenericLister +} + +func (p *protectedLister) List(selector labels.Selector) (ret []runtime.Object, err error) { + if !p.hasSynced() { + return nil, p.notReadyErr + } + return p.delegate.List(selector) +} +func (p *protectedLister) Get(name string) (runtime.Object, error) { + if !p.hasSynced() { + return nil, p.notReadyErr + } + return p.delegate.Get(name) +} +func (p *protectedLister) ByNamespace(namespace string) cache.GenericNamespaceLister { + return &protectedNamespaceLister{p.hasSynced, p.notReadyErr, p.delegate.ByNamespace(namespace)} +} + +// protectedNamespaceLister returns notReadyError if hasSynced returns false, otherwise delegates to delegate +type protectedNamespaceLister struct { + hasSynced func() bool + notReadyErr error + delegate cache.GenericNamespaceLister +} + +func (p *protectedNamespaceLister) List(selector labels.Selector) (ret []runtime.Object, err error) { + if !p.hasSynced() { + return nil, p.notReadyErr + } + return p.delegate.List(selector) +} +func (p *protectedNamespaceLister) Get(name string) (runtime.Object, error) { + if !p.hasSynced() { + return nil, p.notReadyErr + } + return p.delegate.Get(name) +} + +// ListResourceUsingListerFunc returns a listing function based on the shared informer factory for the specified resource. +func ListResourceUsingListerFunc(l quota.ListerForResourceFunc, resource schema.GroupVersionResource) ListFuncByNamespace { + return func(namespace string) ([]runtime.Object, error) { + lister, err := l(resource) + if err != nil { + return nil, err + } + return lister.ByNamespace(namespace).List(labels.Everything()) + } +} + +// ObjectCountQuotaResourceNameFor returns the object count quota name for specified groupResource +func ObjectCountQuotaResourceNameFor(groupResource schema.GroupResource) corev1.ResourceName { + if len(groupResource.Group) == 0 { + return corev1.ResourceName("count/" + groupResource.Resource) + } + return corev1.ResourceName("count/" + groupResource.Resource + "." + groupResource.Group) +} + +// ListFuncByNamespace knows how to list resources in a namespace +type ListFuncByNamespace func(namespace string) ([]runtime.Object, error) + +// MatchesScopeFunc knows how to evaluate if an object matches a scope +type MatchesScopeFunc func(scope corev1.ScopedResourceSelectorRequirement, object runtime.Object) (bool, error) + +// UsageFunc knows how to measure usage associated with an object +type UsageFunc func(object runtime.Object) (corev1.ResourceList, error) + +// MatchingResourceNamesFunc is a function that returns the list of resources matched +type MatchingResourceNamesFunc func(input []corev1.ResourceName) []corev1.ResourceName + +// MatchesNoScopeFunc returns false on all match checks +func MatchesNoScopeFunc(scope corev1.ScopedResourceSelectorRequirement, object runtime.Object) (bool, error) { + return false, nil +} + +// Matches returns true if the quota matches the specified item. +func Matches( + resourceQuota *corev1.ResourceQuota, item runtime.Object, + matchFunc MatchingResourceNamesFunc, scopeFunc MatchesScopeFunc) (bool, error) { + if resourceQuota == nil { + return false, fmt.Errorf("expected non-nil quota") + } + // verify the quota matches on at least one resource + matchResource := len(matchFunc(quota.ResourceNames(resourceQuota.Status.Hard))) > 0 + // by default, no scopes matches all + matchScope := true + for _, scope := range getScopeSelectorsFromQuota(resourceQuota) { + innerMatch, err := scopeFunc(scope, item) + if err != nil { + return false, err + } + matchScope = matchScope && innerMatch + } + return matchResource && matchScope, nil +} + +func getScopeSelectorsFromQuota(quota *corev1.ResourceQuota) []corev1.ScopedResourceSelectorRequirement { + selectors := []corev1.ScopedResourceSelectorRequirement{} + for _, scope := range quota.Spec.Scopes { + selectors = append(selectors, corev1.ScopedResourceSelectorRequirement{ + ScopeName: scope, + Operator: corev1.ScopeSelectorOpExists}) + } + if quota.Spec.ScopeSelector != nil { + selectors = append(selectors, quota.Spec.ScopeSelector.MatchExpressions...) + } + return selectors +} + +// CalculateUsageStats is a utility function that knows how to calculate aggregate usage. +func CalculateUsageStats(options quota.UsageStatsOptions, + listFunc ListFuncByNamespace, + scopeFunc MatchesScopeFunc, + usageFunc UsageFunc) (quota.UsageStats, error) { + // default each tracked resource to zero + result := quota.UsageStats{Used: corev1.ResourceList{}} + for _, resourceName := range options.Resources { + result.Used[resourceName] = resource.Quantity{Format: resource.DecimalSI} + } + items, err := listFunc(options.Namespace) + if err != nil { + return result, fmt.Errorf("failed to list content: %v", err) + } + for _, item := range items { + // need to verify that the item matches the set of scopes + matchesScopes := true + for _, scope := range options.Scopes { + innerMatch, err := scopeFunc(corev1.ScopedResourceSelectorRequirement{ScopeName: scope}, item) + if err != nil { + return result, nil + } + if !innerMatch { + matchesScopes = false + } + } + if options.ScopeSelector != nil { + for _, selector := range options.ScopeSelector.MatchExpressions { + innerMatch, err := scopeFunc(selector, item) + if err != nil { + return result, nil + } + matchesScopes = matchesScopes && innerMatch + } + } + // only count usage if there was a match + if matchesScopes { + usage, err := usageFunc(item) + if err != nil { + return result, err + } + result.Used = quota.Add(result.Used, usage) + } + } + return result, nil +} + +// objectCountEvaluator provides an implementation for quota.Evaluator +// that associates usage of the specified resource based on the number of items +// returned by the specified listing function. +type objectCountEvaluator struct { + // GroupResource that this evaluator tracks. + // It is used to construct a generic object count quota name + groupResource schema.GroupResource + // A function that knows how to list resources by namespace. + // TODO move to dynamic client in future + listFuncByNamespace ListFuncByNamespace + // Names associated with this resource in the quota for generic counting. + resourceNames []corev1.ResourceName +} + +// Constraints returns an error if the configured resource name is not in the required set. +func (o *objectCountEvaluator) Constraints(required []corev1.ResourceName, item runtime.Object) error { + // no-op for object counting + return nil +} + +// Handles returns true if the object count evaluator needs to track this attributes. +func (o *objectCountEvaluator) Handles(a admission.Attributes) bool { + operation := a.GetOperation() + return operation == admission.Create +} + +// Matches returns true if the evaluator matches the specified quota with the provided input item +func (o *objectCountEvaluator) Matches(resourceQuota *corev1.ResourceQuota, item runtime.Object) (bool, error) { + return Matches(resourceQuota, item, o.MatchingResources, MatchesNoScopeFunc) +} + +// MatchingResources takes the input specified list of resources and returns the set of resources it matches. +func (o *objectCountEvaluator) MatchingResources(input []corev1.ResourceName) []corev1.ResourceName { + return quota.Intersection(input, o.resourceNames) +} + +// MatchingScopes takes the input specified list of scopes and input object. Returns the set of scopes resource matches. +func (o *objectCountEvaluator) MatchingScopes(item runtime.Object, scopes []corev1.ScopedResourceSelectorRequirement) ([]corev1.ScopedResourceSelectorRequirement, error) { + return []corev1.ScopedResourceSelectorRequirement{}, nil +} + +// UncoveredQuotaScopes takes the input matched scopes which are limited by configuration and the matched quota scopes. +// It returns the scopes which are in limited scopes but dont have a corresponding covering quota scope +func (o *objectCountEvaluator) UncoveredQuotaScopes(limitedScopes []corev1.ScopedResourceSelectorRequirement, matchedQuotaScopes []corev1.ScopedResourceSelectorRequirement) ([]corev1.ScopedResourceSelectorRequirement, error) { + return []corev1.ScopedResourceSelectorRequirement{}, nil +} + +// Usage returns the resource usage for the specified object +func (o *objectCountEvaluator) Usage(object runtime.Object) (corev1.ResourceList, error) { + quantity := resource.NewQuantity(1, resource.DecimalSI) + resourceList := corev1.ResourceList{} + for _, resourceName := range o.resourceNames { + resourceList[resourceName] = *quantity + } + return resourceList, nil +} + +// GroupResource tracked by this evaluator +func (o *objectCountEvaluator) GroupResource() schema.GroupResource { + return o.groupResource +} + +// UsageStats calculates aggregate usage for the object. +func (o *objectCountEvaluator) UsageStats(options quota.UsageStatsOptions) (quota.UsageStats, error) { + return CalculateUsageStats(options, o.listFuncByNamespace, MatchesNoScopeFunc, o.Usage) +} + +// Verify implementation of interface at compile time. +var _ quota.Evaluator = &objectCountEvaluator{} + +// NewObjectCountEvaluator returns an evaluator that can perform generic +// object quota counting. It allows an optional alias for backwards compatibility +// purposes for the legacy object counting names in quota. Unless its supporting +// backward compatibility, alias should not be used. +func NewObjectCountEvaluator( + groupResource schema.GroupResource, listFuncByNamespace ListFuncByNamespace, + alias corev1.ResourceName) quota.Evaluator { + + resourceNames := []corev1.ResourceName{ObjectCountQuotaResourceNameFor(groupResource)} + if len(alias) > 0 { + resourceNames = append(resourceNames, alias) + } + + return &objectCountEvaluator{ + groupResource: groupResource, + listFuncByNamespace: listFuncByNamespace, + resourceNames: resourceNames, + } +} diff --git a/kube/pkg/quota/v1/generic/registry.go b/kube/pkg/quota/v1/generic/registry.go new file mode 100644 index 000000000..d31ac699e --- /dev/null +++ b/kube/pkg/quota/v1/generic/registry.go @@ -0,0 +1,81 @@ +/* +Copyright 2016 The Kubernetes 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. +*/ + +package generic + +import ( + "sync" + + "k8s.io/apimachinery/pkg/runtime/schema" + quota "kubesphere.io/kubesphere/kube/pkg/quota/v1" +) + +// implements a basic registry +type simpleRegistry struct { + lock sync.RWMutex + // evaluators tracked by the registry + evaluators map[schema.GroupResource]quota.Evaluator +} + +// NewRegistry creates a simple registry with initial list of evaluators +func NewRegistry(evaluators []quota.Evaluator) quota.Registry { + return &simpleRegistry{ + evaluators: evaluatorsByGroupResource(evaluators), + } +} + +func (r *simpleRegistry) Add(e quota.Evaluator) { + r.lock.Lock() + defer r.lock.Unlock() + r.evaluators[e.GroupResource()] = e +} + +func (r *simpleRegistry) Remove(e quota.Evaluator) { + r.lock.Lock() + defer r.lock.Unlock() + delete(r.evaluators, e.GroupResource()) +} + +func (r *simpleRegistry) Get(gr schema.GroupResource) quota.Evaluator { + r.lock.RLock() + defer r.lock.RUnlock() + return r.evaluators[gr] +} + +func (r *simpleRegistry) List() []quota.Evaluator { + r.lock.RLock() + defer r.lock.RUnlock() + + return evaluatorsList(r.evaluators) +} + +// evaluatorsByGroupResource converts a list of evaluators to a map by group resource. +func evaluatorsByGroupResource(items []quota.Evaluator) map[schema.GroupResource]quota.Evaluator { + result := map[schema.GroupResource]quota.Evaluator{} + for _, item := range items { + result[item.GroupResource()] = item + } + return result +} + +// evaluatorsList converts a map of evaluators to list +func evaluatorsList(input map[schema.GroupResource]quota.Evaluator) []quota.Evaluator { + var result []quota.Evaluator + for _, item := range input { + result = append(result, item) + } + return result +} diff --git a/kube/pkg/quota/v1/install/registry.go b/kube/pkg/quota/v1/install/registry.go new file mode 100644 index 000000000..4e8179790 --- /dev/null +++ b/kube/pkg/quota/v1/install/registry.go @@ -0,0 +1,47 @@ +/* +Copyright 2016 The Kubernetes 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. +*/ + +package install + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + quota "kubesphere.io/kubesphere/kube/pkg/quota/v1" + core "kubesphere.io/kubesphere/kube/pkg/quota/v1/evaluator/core" + generic "kubesphere.io/kubesphere/kube/pkg/quota/v1/generic" +) + +// NewQuotaConfigurationForAdmission returns a quota configuration for admission control. +func NewQuotaConfigurationForAdmission() quota.Configuration { + evaluators := core.NewEvaluators(nil) + return generic.NewConfiguration(evaluators, DefaultIgnoredResources()) +} + +// NewQuotaConfigurationForControllers returns a quota configuration for controllers. +func NewQuotaConfigurationForControllers(f quota.ListerForResourceFunc) quota.Configuration { + evaluators := core.NewEvaluators(f) + return generic.NewConfiguration(evaluators, DefaultIgnoredResources()) +} + +// ignoredResources are ignored by quota by default +var ignoredResources = map[schema.GroupResource]struct{}{ + {Group: "", Resource: "events"}: {}, +} + +// DefaultIgnoredResources returns the default set of resources that quota system +// should ignore. This is exposed so downstream integrators can have access to them. +func DefaultIgnoredResources() map[schema.GroupResource]struct{} { + return ignoredResources +} diff --git a/kube/pkg/quota/v1/interfaces.go b/kube/pkg/quota/v1/interfaces.go new file mode 100644 index 000000000..d71b66418 --- /dev/null +++ b/kube/pkg/quota/v1/interfaces.go @@ -0,0 +1,88 @@ +/* +Copyright 2016 The Kubernetes 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. +*/ + +package quota + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/admission" + "k8s.io/client-go/tools/cache" +) + +// UsageStatsOptions is an options structs that describes how stats should be calculated +type UsageStatsOptions struct { + // Namespace where stats should be calculate + Namespace string + // Scopes that must match counted objects + Scopes []corev1.ResourceQuotaScope + // Resources are the set of resources to include in the measurement + Resources []corev1.ResourceName + ScopeSelector *corev1.ScopeSelector +} + +// UsageStats is result of measuring observed resource use in the system +type UsageStats struct { + // Used maps resource to quantity used + Used corev1.ResourceList +} + +// Evaluator knows how to evaluate quota usage for a particular group resource +type Evaluator interface { + // Constraints ensures that each required resource is present on item + Constraints(required []corev1.ResourceName, item runtime.Object) error + // GroupResource returns the groupResource that this object knows how to evaluate + GroupResource() schema.GroupResource + // Handles determines if quota could be impacted by the specified attribute. + // If true, admission control must perform quota processing for the operation, otherwise it is safe to ignore quota. + Handles(operation admission.Attributes) bool + // Matches returns true if the specified quota matches the input item + Matches(resourceQuota *corev1.ResourceQuota, item runtime.Object) (bool, error) + // MatchingScopes takes the input specified list of scopes and input object and returns the set of scopes that matches input object. + MatchingScopes(item runtime.Object, scopes []corev1.ScopedResourceSelectorRequirement) ([]corev1.ScopedResourceSelectorRequirement, error) + // UncoveredQuotaScopes takes the input matched scopes which are limited by configuration and the matched quota scopes. It returns the scopes which are in limited scopes but dont have a corresponding covering quota scope + UncoveredQuotaScopes(limitedScopes []corev1.ScopedResourceSelectorRequirement, matchedQuotaScopes []corev1.ScopedResourceSelectorRequirement) ([]corev1.ScopedResourceSelectorRequirement, error) + // MatchingResources takes the input specified list of resources and returns the set of resources evaluator matches. + MatchingResources(input []corev1.ResourceName) []corev1.ResourceName + // Usage returns the resource usage for the specified object + Usage(item runtime.Object) (corev1.ResourceList, error) + // UsageStats calculates latest observed usage stats for all objects + UsageStats(options UsageStatsOptions) (UsageStats, error) +} + +// Configuration defines how the quota system is configured. +type Configuration interface { + // IgnoredResources are ignored by quota. + IgnoredResources() map[schema.GroupResource]struct{} + // Evaluators for quota evaluation. + Evaluators() []Evaluator +} + +// Registry maintains a list of evaluators +type Registry interface { + // Add to registry + Add(e Evaluator) + // Remove from registry + Remove(e Evaluator) + // Get by group resource + Get(gr schema.GroupResource) Evaluator + // List from registry + List() []Evaluator +} + +// ListerForResourceFunc knows how to get a lister for a specific resource +type ListerForResourceFunc func(schema.GroupVersionResource) (cache.GenericLister, error) diff --git a/kube/pkg/quota/v1/resources.go b/kube/pkg/quota/v1/resources.go new file mode 100644 index 000000000..9b217489d --- /dev/null +++ b/kube/pkg/quota/v1/resources.go @@ -0,0 +1,293 @@ +/* +Copyright 2016 The Kubernetes 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. +*/ + +package quota + +import ( + "sort" + "strings" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/sets" +) + +// Equals returns true if the two lists are equivalent +func Equals(a corev1.ResourceList, b corev1.ResourceList) bool { + if len(a) != len(b) { + return false + } + + for key, value1 := range a { + value2, found := b[key] + if !found { + return false + } + if value1.Cmp(value2) != 0 { + return false + } + } + + return true +} + +// LessThanOrEqual returns true if a < b for each key in b +// If false, it returns the keys in a that exceeded b +func LessThanOrEqual(a corev1.ResourceList, b corev1.ResourceList) (bool, []corev1.ResourceName) { + result := true + resourceNames := []corev1.ResourceName{} + for key, value := range b { + if other, found := a[key]; found { + if other.Cmp(value) > 0 { + result = false + resourceNames = append(resourceNames, key) + } + } + } + return result, resourceNames +} + +// Max returns the result of Max(a, b) for each named resource +func Max(a corev1.ResourceList, b corev1.ResourceList) corev1.ResourceList { + result := corev1.ResourceList{} + for key, value := range a { + if other, found := b[key]; found { + if value.Cmp(other) <= 0 { + result[key] = other.DeepCopy() + continue + } + } + result[key] = value.DeepCopy() + } + for key, value := range b { + if _, found := result[key]; !found { + result[key] = value.DeepCopy() + } + } + return result +} + +// Add returns the result of a + b for each named resource +func Add(a corev1.ResourceList, b corev1.ResourceList) corev1.ResourceList { + result := corev1.ResourceList{} + for key, value := range a { + quantity := value.DeepCopy() + if other, found := b[key]; found { + quantity.Add(other) + } + result[key] = quantity + } + for key, value := range b { + if _, found := result[key]; !found { + result[key] = value.DeepCopy() + } + } + return result +} + +// SubtractWithNonNegativeResult - subtracts and returns result of a - b but +// makes sure we don't return negative values to prevent negative resource usage. +func SubtractWithNonNegativeResult(a corev1.ResourceList, b corev1.ResourceList) corev1.ResourceList { + zero := resource.MustParse("0") + + result := corev1.ResourceList{} + for key, value := range a { + quantity := value.DeepCopy() + if other, found := b[key]; found { + quantity.Sub(other) + } + if quantity.Cmp(zero) > 0 { + result[key] = quantity + } else { + result[key] = zero + } + } + + for key := range b { + if _, found := result[key]; !found { + result[key] = zero + } + } + return result +} + +// Subtract returns the result of a - b for each named resource +func Subtract(a corev1.ResourceList, b corev1.ResourceList) corev1.ResourceList { + result := corev1.ResourceList{} + for key, value := range a { + quantity := value.DeepCopy() + if other, found := b[key]; found { + quantity.Sub(other) + } + result[key] = quantity + } + for key, value := range b { + if _, found := result[key]; !found { + quantity := value.DeepCopy() + quantity.Neg() + result[key] = quantity + } + } + return result +} + +// Mask returns a new resource list that only has the values with the specified names +func Mask(resources corev1.ResourceList, names []corev1.ResourceName) corev1.ResourceList { + nameSet := ToSet(names) + result := corev1.ResourceList{} + for key, value := range resources { + if nameSet.Has(string(key)) { + result[key] = value.DeepCopy() + } + } + return result +} + +// ResourceNames returns a list of all resource names in the ResourceList +func ResourceNames(resources corev1.ResourceList) []corev1.ResourceName { + result := []corev1.ResourceName{} + for resourceName := range resources { + result = append(result, resourceName) + } + return result +} + +// Contains returns true if the specified item is in the list of items +func Contains(items []corev1.ResourceName, item corev1.ResourceName) bool { + for _, i := range items { + if i == item { + return true + } + } + return false +} + +// ContainsPrefix returns true if the specified item has a prefix that contained in given prefix Set +func ContainsPrefix(prefixSet []string, item corev1.ResourceName) bool { + for _, prefix := range prefixSet { + if strings.HasPrefix(string(item), prefix) { + return true + } + } + return false +} + +// Intersection returns the intersection of both list of resources, deduped and sorted +func Intersection(a []corev1.ResourceName, b []corev1.ResourceName) []corev1.ResourceName { + result := make([]corev1.ResourceName, 0, len(a)) + for _, item := range a { + if Contains(result, item) { + continue + } + if !Contains(b, item) { + continue + } + result = append(result, item) + } + sort.Slice(result, func(i, j int) bool { return result[i] < result[j] }) + return result +} + +// Difference returns the list of resources resulting from a-b, deduped and sorted +func Difference(a []corev1.ResourceName, b []corev1.ResourceName) []corev1.ResourceName { + result := make([]corev1.ResourceName, 0, len(a)) + for _, item := range a { + if Contains(b, item) || Contains(result, item) { + continue + } + result = append(result, item) + } + sort.Slice(result, func(i, j int) bool { return result[i] < result[j] }) + return result +} + +// IsZero returns true if each key maps to the quantity value 0 +func IsZero(a corev1.ResourceList) bool { + zero := resource.MustParse("0") + for _, v := range a { + if v.Cmp(zero) != 0 { + return false + } + } + return true +} + +// IsNegative returns the set of resource names that have a negative value. +func IsNegative(a corev1.ResourceList) []corev1.ResourceName { + results := []corev1.ResourceName{} + zero := resource.MustParse("0") + for k, v := range a { + if v.Cmp(zero) < 0 { + results = append(results, k) + } + } + return results +} + +// ToSet takes a list of resource names and converts to a string set +func ToSet(resourceNames []corev1.ResourceName) sets.String { + result := sets.NewString() + for _, resourceName := range resourceNames { + result.Insert(string(resourceName)) + } + return result +} + +// CalculateUsage calculates and returns the requested ResourceList usage. +// If an error is returned, usage only contains the resources which encountered no calculation errors. +func CalculateUsage(namespaceName string, scopes []corev1.ResourceQuotaScope, hardLimits corev1.ResourceList, registry Registry, scopeSelector *corev1.ScopeSelector) (corev1.ResourceList, error) { + // find the intersection between the hard resources on the quota + // and the resources this controller can track to know what we can + // look to measure updated usage stats for + hardResources := ResourceNames(hardLimits) + potentialResources := []corev1.ResourceName{} + evaluators := registry.List() + for _, evaluator := range evaluators { + potentialResources = append(potentialResources, evaluator.MatchingResources(hardResources)...) + } + // NOTE: the intersection just removes duplicates since the evaluator match intersects with hard + matchedResources := Intersection(hardResources, potentialResources) + + errors := []error{} + + // sum the observed usage from each evaluator + newUsage := corev1.ResourceList{} + for _, evaluator := range evaluators { + // only trigger the evaluator if it matches a resource in the quota, otherwise, skip calculating anything + intersection := evaluator.MatchingResources(matchedResources) + if len(intersection) == 0 { + continue + } + + usageStatsOptions := UsageStatsOptions{Namespace: namespaceName, Scopes: scopes, Resources: intersection, ScopeSelector: scopeSelector} + stats, err := evaluator.UsageStats(usageStatsOptions) + if err != nil { + // remember the error + errors = append(errors, err) + // exclude resources which encountered calculation errors + matchedResources = Difference(matchedResources, intersection) + continue + } + newUsage = Add(newUsage, stats.Used) + } + + // mask the observed usage to only the set of resources tracked by this quota + // merge our observed usage with the quota usage status + // if the new usage is different than the last usage, we will need to do an update + newUsage = Mask(newUsage, matchedResources) + return newUsage, utilerrors.NewAggregate(errors) +} diff --git a/kube/plugin/pkg/admission/resourcequota/admission.go b/kube/plugin/pkg/admission/resourcequota/admission.go new file mode 100644 index 000000000..f3a82aa42 --- /dev/null +++ b/kube/plugin/pkg/admission/resourcequota/admission.go @@ -0,0 +1,126 @@ +/* +Copyright 2014 The Kubernetes 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. +*/ + +package resourcequota + +import ( + "context" + "fmt" + "time" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apiserver/pkg/admission" + genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + "kubesphere.io/kubesphere/kube/pkg/quota/v1" + "kubesphere.io/kubesphere/kube/pkg/quota/v1/generic" + resourcequotaapi "kubesphere.io/kubesphere/kube/plugin/pkg/admission/resourcequota/apis/resourcequota" +) + +// QuotaAdmission implements an admission controller that can enforce quota constraints +type QuotaAdmission struct { + *admission.Handler + config *resourcequotaapi.Configuration + stopCh <-chan struct{} + quotaConfiguration quota.Configuration + numEvaluators int + quotaAccessor *quotaAccessor + evaluator Evaluator +} + +// WantsQuotaConfiguration defines a function which sets quota configuration for admission plugins that need it. +type WantsQuotaConfiguration interface { + SetQuotaConfiguration(quota.Configuration) + admission.InitializationValidator +} + +var _ admission.ValidationInterface = &QuotaAdmission{} +var _ = genericadmissioninitializer.WantsExternalKubeInformerFactory(&QuotaAdmission{}) +var _ = genericadmissioninitializer.WantsExternalKubeClientSet(&QuotaAdmission{}) +var _ = WantsQuotaConfiguration(&QuotaAdmission{}) + +type liveLookupEntry struct { + expiry time.Time + items []*corev1.ResourceQuota +} + +// NewResourceQuota configures an admission controller that can enforce quota constraints +// using the provided registry. The registry must have the capability to handle group/kinds that +// are persisted by the server this admission controller is intercepting +func NewResourceQuota(config *resourcequotaapi.Configuration, numEvaluators int, stopCh <-chan struct{}) (*QuotaAdmission, error) { + quotaAccessor, err := newQuotaAccessor() + if err != nil { + return nil, err + } + + return &QuotaAdmission{ + Handler: admission.NewHandler(admission.Create, admission.Update), + stopCh: stopCh, + numEvaluators: numEvaluators, + config: config, + quotaAccessor: quotaAccessor, + }, nil +} + +// SetExternalKubeClientSet registers the client into QuotaAdmission +func (a *QuotaAdmission) SetExternalKubeClientSet(client kubernetes.Interface) { + a.quotaAccessor.client = client +} + +// SetExternalKubeInformerFactory registers an informer factory into QuotaAdmission +func (a *QuotaAdmission) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) { + a.quotaAccessor.lister = f.Core().V1().ResourceQuotas().Lister() +} + +// SetQuotaConfiguration assigns and initializes configuration and evaluator for QuotaAdmission +func (a *QuotaAdmission) SetQuotaConfiguration(c quota.Configuration) { + a.quotaConfiguration = c + a.evaluator = NewQuotaEvaluator(a.quotaAccessor, a.quotaConfiguration.IgnoredResources(), generic.NewRegistry(a.quotaConfiguration.Evaluators()), nil, a.config, a.numEvaluators, a.stopCh) +} + +// ValidateInitialization ensures an authorizer is set. +func (a *QuotaAdmission) ValidateInitialization() error { + if a.quotaAccessor == nil { + return fmt.Errorf("missing quotaAccessor") + } + if a.quotaAccessor.client == nil { + return fmt.Errorf("missing quotaAccessor.client") + } + if a.quotaAccessor.lister == nil { + return fmt.Errorf("missing quotaAccessor.lister") + } + if a.quotaConfiguration == nil { + return fmt.Errorf("missing quotaConfiguration") + } + if a.evaluator == nil { + return fmt.Errorf("missing evaluator") + } + return nil +} + +// Validate makes admission decisions while enforcing quota +func (a *QuotaAdmission) Validate(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) (err error) { + // ignore all operations that correspond to sub-resource actions + if attr.GetSubresource() != "" { + return nil + } + // ignore all operations that are not namespaced + if attr.GetNamespace() == "" { + return nil + } + return a.evaluator.Evaluate(attr) +} diff --git a/kube/plugin/pkg/admission/resourcequota/apis/resourcequota/types.go b/kube/plugin/pkg/admission/resourcequota/apis/resourcequota/types.go new file mode 100644 index 000000000..a20d0b0d2 --- /dev/null +++ b/kube/plugin/pkg/admission/resourcequota/apis/resourcequota/types.go @@ -0,0 +1,74 @@ +/* + + Copyright 2021 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. + +*/ + +package resourcequota + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// Configuration provides configuration for the ResourceQuota admission controller. +type Configuration struct { + metav1.TypeMeta + + // LimitedResources whose consumption is limited by default. + // +optional + LimitedResources []LimitedResource +} + +// LimitedResource matches a resource whose consumption is limited by default. +// To consume the resource, there must exist an associated quota that limits +// its consumption. +type LimitedResource struct { + + // APIGroup is the name of the APIGroup that contains the limited resource. + // +optional + APIGroup string `json:"apiGroup,omitempty"` + + // Resource is the name of the resource this rule applies to. + // For example, if the administrator wants to limit consumption + // of a storage resource associated with persistent volume claims, + // the value would be "persistentvolumeclaims". + Resource string `json:"resource"` + + // For each intercepted request, the quota system will evaluate + // its resource usage. It will iterate through each resource consumed + // and if the resource contains any substring in this listing, the + // quota system will ensure that there is a covering quota. In the + // absence of a covering quota, the quota system will deny the request. + // For example, if an administrator wants to globally enforce that + // that a quota must exist to consume persistent volume claims associated + // with any storage class, the list would include + // ".storageclass.storage.k8s.io/requests.storage" + MatchContains []string + + // For each intercepted request, the quota system will figure out if the input object + // satisfies a scope which is present in this listing, then + // quota system will ensure that there is a covering quota. In the + // absence of a covering quota, the quota system will deny the request. + // For example, if an administrator wants to globally enforce that + // a quota must exist to create a pod with "cluster-services" priorityclass + // the list would include + // "PriorityClassNameIn=cluster-services" + // +optional + // MatchScopes []string `json:"matchScopes,omitempty"` + MatchScopes []corev1.ScopedResourceSelectorRequirement `json:"matchScopes,omitempty"` +} diff --git a/kube/plugin/pkg/admission/resourcequota/controller.go b/kube/plugin/pkg/admission/resourcequota/controller.go new file mode 100644 index 000000000..6d15c13a3 --- /dev/null +++ b/kube/plugin/pkg/admission/resourcequota/controller.go @@ -0,0 +1,717 @@ +/* +Copyright 2016 The Kubernetes 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. +*/ + +package resourcequota + +import ( + "fmt" + "sort" + "strings" + "sync" + "time" + + "k8s.io/klog" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apiserver/pkg/admission" + "k8s.io/client-go/util/workqueue" + quota "kubesphere.io/kubesphere/kube/pkg/quota/v1" + "kubesphere.io/kubesphere/kube/pkg/quota/v1/generic" + resourcequotaapi "kubesphere.io/kubesphere/kube/plugin/pkg/admission/resourcequota/apis/resourcequota" +) + +// Evaluator is used to see if quota constraints are satisfied. +type Evaluator interface { + // Evaluate takes an operation and checks to see if quota constraints are satisfied. It returns an error if they are not. + // The default implementation process related operations in chunks when possible. + Evaluate(a admission.Attributes) error +} + +type quotaEvaluator struct { + quotaAccessor QuotaAccessor + // lockAcquisitionFunc acquires any required locks and returns a cleanup method to defer + lockAcquisitionFunc func([]corev1.ResourceQuota) func() + + ignoredResources map[schema.GroupResource]struct{} + + // registry that knows how to measure usage for objects + registry quota.Registry + + // TODO these are used together to bucket items by namespace and then batch them up for processing. + // The technique is valuable for rollup activities to avoid fanout and reduce resource contention. + // We could move this into a library if another component needed it. + // queue is indexed by namespace, so that we bundle up on a per-namespace basis + queue *workqueue.Type + workLock sync.Mutex + work map[string][]*admissionWaiter + dirtyWork map[string][]*admissionWaiter + inProgress sets.String + + // controls the run method so that we can cleanly conform to the Evaluator interface + workers int + stopCh <-chan struct{} + init sync.Once + + // lets us know what resources are limited by default + config *resourcequotaapi.Configuration +} + +type admissionWaiter struct { + attributes admission.Attributes + finished chan struct{} + result error +} + +type defaultDeny struct{} + +func (defaultDeny) Error() string { + return "DEFAULT DENY" +} + +// IsDefaultDeny returns true if the error is defaultDeny +func IsDefaultDeny(err error) bool { + if err == nil { + return false + } + + _, ok := err.(defaultDeny) + return ok +} + +func newAdmissionWaiter(a admission.Attributes) *admissionWaiter { + return &admissionWaiter{ + attributes: a, + finished: make(chan struct{}), + result: defaultDeny{}, + } +} + +// NewQuotaEvaluator configures an admission controller that can enforce quota constraints +// using the provided registry. The registry must have the capability to handle group/kinds that +// are persisted by the server this admission controller is intercepting +func NewQuotaEvaluator(quotaAccessor QuotaAccessor, ignoredResources map[schema.GroupResource]struct{}, quotaRegistry quota.Registry, lockAcquisitionFunc func([]corev1.ResourceQuota) func(), config *resourcequotaapi.Configuration, workers int, stopCh <-chan struct{}) Evaluator { + // if we get a nil config, just create an empty default. + if config == nil { + config = &resourcequotaapi.Configuration{} + } + + return "aEvaluator{ + quotaAccessor: quotaAccessor, + lockAcquisitionFunc: lockAcquisitionFunc, + + ignoredResources: ignoredResources, + registry: quotaRegistry, + + queue: workqueue.NewNamed("admission_quota_controller"), + work: map[string][]*admissionWaiter{}, + dirtyWork: map[string][]*admissionWaiter{}, + inProgress: sets.String{}, + + workers: workers, + stopCh: stopCh, + config: config, + } +} + +// Run begins watching and syncing. +func (e *quotaEvaluator) run() { + defer utilruntime.HandleCrash() + + for i := 0; i < e.workers; i++ { + go wait.Until(e.doWork, time.Second, e.stopCh) + } + <-e.stopCh + klog.Infof("Shutting down quota evaluator") + e.queue.ShutDown() +} + +func (e *quotaEvaluator) doWork() { + workFunc := func() bool { + ns, admissionAttributes, quit := e.getWork() + if quit { + return true + } + defer e.completeWork(ns) + if len(admissionAttributes) == 0 { + return false + } + e.checkAttributes(ns, admissionAttributes) + return false + } + for { + if quit := workFunc(); quit { + klog.Infof("quota evaluator worker shutdown") + return + } + } +} + +// checkAttributes iterates evaluates all the waiting admissionAttributes. It will always notify all waiters +// before returning. The default is to deny. +func (e *quotaEvaluator) checkAttributes(ns string, admissionAttributes []*admissionWaiter) { + // notify all on exit + defer func() { + for _, admissionAttribute := range admissionAttributes { + close(admissionAttribute.finished) + } + }() + + quotas, err := e.quotaAccessor.GetQuotas(ns) + if err != nil { + for _, admissionAttribute := range admissionAttributes { + admissionAttribute.result = err + } + return + } + // if limited resources are disabled, we can just return safely when there are no quotas. + limitedResourcesDisabled := len(e.config.LimitedResources) == 0 + if len(quotas) == 0 && limitedResourcesDisabled { + for _, admissionAttribute := range admissionAttributes { + admissionAttribute.result = nil + } + return + } + + if e.lockAcquisitionFunc != nil { + releaseLocks := e.lockAcquisitionFunc(quotas) + defer releaseLocks() + } + + e.checkQuotas(quotas, admissionAttributes, 3) +} + +// checkQuotas checks the admission attributes against the passed quotas. If a quota applies, it will attempt to update it +// AFTER it has checked all the admissionAttributes. The method breaks down into phase like this: +// 0. make a copy of the quotas to act as a "running" quota so we know what we need to update and can still compare against the +// originals +// 1. check each admission attribute to see if it fits within *all* the quotas. If it doesn't fit, mark the waiter as failed +// and the running quota don't change. If it did fit, check to see if any quota was changed. It there was no quota change +// mark the waiter as succeeded. If some quota did change, update the running quotas +// 2. If no running quota was changed, return now since no updates are needed. +// 3. for each quota that has changed, attempt an update. If all updates succeeded, update all unset waiters to success status and return. If the some +// updates failed on conflict errors and we have retries left, re-get the failed quota from our cache for the latest version +// and recurse into this method with the subset. It's safe for us to evaluate ONLY the subset, because the other quota +// documents for these waiters have already been evaluated. Step 1, will mark all the ones that should already have succeeded. +func (e *quotaEvaluator) checkQuotas(quotas []corev1.ResourceQuota, admissionAttributes []*admissionWaiter, remainingRetries int) { + // yet another copy to compare against originals to see if we actually have deltas + originalQuotas, err := copyQuotas(quotas) + if err != nil { + utilruntime.HandleError(err) + return + } + + atLeastOneChanged := false + for i := range admissionAttributes { + admissionAttribute := admissionAttributes[i] + newQuotas, err := e.checkRequest(quotas, admissionAttribute.attributes) + if err != nil { + admissionAttribute.result = err + continue + } + + // Don't update quota for admissionAttributes that correspond to dry-run requests + if admissionAttribute.attributes.IsDryRun() { + admissionAttribute.result = nil + continue + } + + // if the new quotas are the same as the old quotas, then this particular one doesn't issue any updates + // that means that no quota docs applied, so it can get a pass + atLeastOneChangeForThisWaiter := false + for j := range newQuotas { + if !quota.Equals(quotas[j].Status.Used, newQuotas[j].Status.Used) { + atLeastOneChanged = true + atLeastOneChangeForThisWaiter = true + break + } + } + + if !atLeastOneChangeForThisWaiter { + admissionAttribute.result = nil + } + + quotas = newQuotas + } + + // if none of the requests changed anything, there's no reason to issue an update, just fail them all now + if !atLeastOneChanged { + return + } + + // now go through and try to issue updates. Things get a little weird here: + // 1. check to see if the quota changed. If not, skip. + // 2. if the quota changed and the update passes, be happy + // 3. if the quota changed and the update fails, add the original to a retry list + var updatedFailedQuotas []corev1.ResourceQuota + var lastErr error + for i := range quotas { + newQuota := quotas[i] + + // if this quota didn't have its status changed, skip it + if quota.Equals(originalQuotas[i].Status.Used, newQuota.Status.Used) { + continue + } + + if err := e.quotaAccessor.UpdateQuotaStatus(&newQuota); err != nil { + updatedFailedQuotas = append(updatedFailedQuotas, newQuota) + lastErr = err + } + } + + if len(updatedFailedQuotas) == 0 { + // all the updates succeeded. At this point, anything with the default deny error was just waiting to + // get a successful update, so we can mark and notify + for _, admissionAttribute := range admissionAttributes { + if IsDefaultDeny(admissionAttribute.result) { + admissionAttribute.result = nil + } + } + return + } + + // at this point, errors are fatal. Update all waiters without status to failed and return + if remainingRetries <= 0 { + for _, admissionAttribute := range admissionAttributes { + if IsDefaultDeny(admissionAttribute.result) { + admissionAttribute.result = lastErr + } + } + return + } + + // this retry logic has the same bug that its possible to be checking against quota in a state that never actually exists where + // you've added a new documented, then updated an old one, your resource matches both and you're only checking one + // updates for these quota names failed. Get the current quotas in the namespace, compare by name, check to see if the + // resource versions have changed. If not, we're going to fall through an fail everything. If they all have, then we can try again + newQuotas, err := e.quotaAccessor.GetQuotas(quotas[0].Namespace) + if err != nil { + // this means that updates failed. Anything with a default deny error has failed and we need to let them know + for _, admissionAttribute := range admissionAttributes { + if IsDefaultDeny(admissionAttribute.result) { + admissionAttribute.result = lastErr + } + } + return + } + + // this logic goes through our cache to find the new version of all quotas that failed update. If something has been removed + // it is skipped on this retry. After all, you removed it. + quotasToCheck := []corev1.ResourceQuota{} + for _, newQuota := range newQuotas { + for _, oldQuota := range updatedFailedQuotas { + if newQuota.Name == oldQuota.Name { + quotasToCheck = append(quotasToCheck, newQuota) + break + } + } + } + e.checkQuotas(quotasToCheck, admissionAttributes, remainingRetries-1) +} + +func copyQuotas(in []corev1.ResourceQuota) ([]corev1.ResourceQuota, error) { + out := make([]corev1.ResourceQuota, 0, len(in)) + for _, quota := range in { + out = append(out, *quota.DeepCopy()) + } + + return out, nil +} + +// filterLimitedResourcesByGroupResource filters the input that match the specified groupResource +func filterLimitedResourcesByGroupResource(input []resourcequotaapi.LimitedResource, groupResource schema.GroupResource) []resourcequotaapi.LimitedResource { + result := []resourcequotaapi.LimitedResource{} + for i := range input { + limitedResource := input[i] + limitedGroupResource := schema.GroupResource{Group: limitedResource.APIGroup, Resource: limitedResource.Resource} + if limitedGroupResource == groupResource { + result = append(result, limitedResource) + } + } + return result +} + +// limitedByDefault determines from the specified usage and limitedResources the set of resources names +// that must be present in a covering quota. It returns empty set if it was unable to determine if +// a resource was not limited by default. +func limitedByDefault(usage corev1.ResourceList, limitedResources []resourcequotaapi.LimitedResource) []corev1.ResourceName { + result := []corev1.ResourceName{} + for _, limitedResource := range limitedResources { + for k, v := range usage { + // if a resource is consumed, we need to check if it matches on the limited resource list. + if v.Sign() == 1 { + // if we get a match, we add it to limited set + for _, matchContain := range limitedResource.MatchContains { + if strings.Contains(string(k), matchContain) { + result = append(result, k) + break + } + } + } + } + } + return result +} + +func getMatchedLimitedScopes(evaluator quota.Evaluator, inputObject runtime.Object, limitedResources []resourcequotaapi.LimitedResource) ([]corev1.ScopedResourceSelectorRequirement, error) { + scopes := []corev1.ScopedResourceSelectorRequirement{} + for _, limitedResource := range limitedResources { + matched, err := evaluator.MatchingScopes(inputObject, limitedResource.MatchScopes) + if err != nil { + klog.Errorf("Error while matching limited Scopes: %v", err) + return []corev1.ScopedResourceSelectorRequirement{}, err + } + for _, scope := range matched { + scopes = append(scopes, scope) + } + } + return scopes, nil +} + +// checkRequest verifies that the request does not exceed any quota constraint. it returns a copy of quotas not yet persisted +// that capture what the usage would be if the request succeeded. It return an error if there is insufficient quota to satisfy the request +func (e *quotaEvaluator) checkRequest(quotas []corev1.ResourceQuota, a admission.Attributes) ([]corev1.ResourceQuota, error) { + evaluator := e.registry.Get(a.GetResource().GroupResource()) + if evaluator == nil { + return quotas, nil + } + return CheckRequest(quotas, a, evaluator, e.config.LimitedResources) +} + +// CheckRequest is a static version of quotaEvaluator.checkRequest, possible to be called from outside. +func CheckRequest(quotas []corev1.ResourceQuota, a admission.Attributes, evaluator quota.Evaluator, + limited []resourcequotaapi.LimitedResource) ([]corev1.ResourceQuota, error) { + if !evaluator.Handles(a) { + return quotas, nil + } + + // if we have limited resources enabled for this resource, always calculate usage + inputObject := a.GetObject() + + // Check if object matches AdmissionConfiguration matchScopes + limitedScopes, err := getMatchedLimitedScopes(evaluator, inputObject, limited) + if err != nil { + return quotas, nil + } + + // determine the set of resource names that must exist in a covering quota + limitedResourceNames := []corev1.ResourceName{} + limitedResources := filterLimitedResourcesByGroupResource(limited, a.GetResource().GroupResource()) + if len(limitedResources) > 0 { + deltaUsage, err := evaluator.Usage(inputObject) + if err != nil { + return quotas, err + } + limitedResourceNames = limitedByDefault(deltaUsage, limitedResources) + } + limitedResourceNamesSet := quota.ToSet(limitedResourceNames) + + // find the set of quotas that are pertinent to this request + // reject if we match the quota, but usage is not calculated yet + // reject if the input object does not satisfy quota constraints + // if there are no pertinent quotas, we can just return + interestingQuotaIndexes := []int{} + // track the cumulative set of resources that were required across all quotas + // this is needed to know if we have satisfied any constraints where consumption + // was limited by default. + restrictedResourcesSet := sets.String{} + restrictedScopes := []corev1.ScopedResourceSelectorRequirement{} + for i := range quotas { + resourceQuota := quotas[i] + scopeSelectors := getScopeSelectorsFromQuota(resourceQuota) + localRestrictedScopes, err := evaluator.MatchingScopes(inputObject, scopeSelectors) + if err != nil { + return nil, fmt.Errorf("error matching scopes of quota %s, err: %v", resourceQuota.Name, err) + } + for _, scope := range localRestrictedScopes { + restrictedScopes = append(restrictedScopes, scope) + } + + match, err := evaluator.Matches(&resourceQuota, inputObject) + if err != nil { + klog.Errorf("Error occurred while matching resource quota, %v, against input object. Err: %v", resourceQuota, err) + return quotas, err + } + if !match { + continue + } + + hardResources := quota.ResourceNames(resourceQuota.Status.Hard) + restrictedResources := evaluator.MatchingResources(hardResources) + if err := evaluator.Constraints(restrictedResources, inputObject); err != nil { + return nil, admission.NewForbidden(a, fmt.Errorf("failed quota: %s: %v", resourceQuota.Name, err)) + } + if !hasUsageStats(&resourceQuota, restrictedResources) { + return nil, admission.NewForbidden(a, fmt.Errorf("status unknown for quota: %s, resources: %s", resourceQuota.Name, prettyPrintResourceNames(restrictedResources))) + } + interestingQuotaIndexes = append(interestingQuotaIndexes, i) + localRestrictedResourcesSet := quota.ToSet(restrictedResources) + restrictedResourcesSet.Insert(localRestrictedResourcesSet.List()...) + } + + // Usage of some resources cannot be counted in isolation. For example, when + // the resource represents a number of unique references to external + // resource. In such a case an evaluator needs to process other objects in + // the same namespace which needs to be known. + namespace := a.GetNamespace() + if accessor, err := meta.Accessor(inputObject); namespace != "" && err == nil { + if accessor.GetNamespace() == "" { + accessor.SetNamespace(namespace) + } + } + // there is at least one quota that definitely matches our object + // as a result, we need to measure the usage of this object for quota + // on updates, we need to subtract the previous measured usage + // if usage shows no change, just return since it has no impact on quota + deltaUsage, err := evaluator.Usage(inputObject) + if err != nil { + return quotas, err + } + + // ensure that usage for input object is never negative (this would mean a resource made a negative resource requirement) + if negativeUsage := quota.IsNegative(deltaUsage); len(negativeUsage) > 0 { + return nil, admission.NewForbidden(a, fmt.Errorf("quota usage is negative for resource(s): %s", prettyPrintResourceNames(negativeUsage))) + } + + if admission.Update == a.GetOperation() { + prevItem := a.GetOldObject() + if prevItem == nil { + return nil, admission.NewForbidden(a, fmt.Errorf("unable to get previous usage since prior version of object was not found")) + } + + // if we can definitively determine that this is not a case of "create on update", + // then charge based on the delta. Otherwise, bill the maximum + metadata, err := meta.Accessor(prevItem) + if err == nil && len(metadata.GetResourceVersion()) > 0 { + prevUsage, innerErr := evaluator.Usage(prevItem) + if innerErr != nil { + return quotas, innerErr + } + deltaUsage = quota.SubtractWithNonNegativeResult(deltaUsage, prevUsage) + } + } + + if quota.IsZero(deltaUsage) { + return quotas, nil + } + + // verify that for every resource that had limited by default consumption + // enabled that there was a corresponding quota that covered its use. + // if not, we reject the request. + hasNoCoveringQuota := limitedResourceNamesSet.Difference(restrictedResourcesSet) + if len(hasNoCoveringQuota) > 0 { + return quotas, admission.NewForbidden(a, fmt.Errorf("insufficient quota to consume: %v", strings.Join(hasNoCoveringQuota.List(), ","))) + } + + // verify that for every scope that had limited access enabled + // that there was a corresponding quota that covered it. + // if not, we reject the request. + scopesHasNoCoveringQuota, err := evaluator.UncoveredQuotaScopes(limitedScopes, restrictedScopes) + if err != nil { + return quotas, err + } + if len(scopesHasNoCoveringQuota) > 0 { + return quotas, fmt.Errorf("insufficient quota to match these scopes: %v", scopesHasNoCoveringQuota) + } + + if len(interestingQuotaIndexes) == 0 { + return quotas, nil + } + + outQuotas, err := copyQuotas(quotas) + if err != nil { + return nil, err + } + + for _, index := range interestingQuotaIndexes { + resourceQuota := outQuotas[index] + + hardResources := quota.ResourceNames(resourceQuota.Status.Hard) + requestedUsage := quota.Mask(deltaUsage, hardResources) + newUsage := quota.Add(resourceQuota.Status.Used, requestedUsage) + maskedNewUsage := quota.Mask(newUsage, quota.ResourceNames(requestedUsage)) + + if allowed, exceeded := quota.LessThanOrEqual(maskedNewUsage, resourceQuota.Status.Hard); !allowed { + failedRequestedUsage := quota.Mask(requestedUsage, exceeded) + failedUsed := quota.Mask(resourceQuota.Status.Used, exceeded) + failedHard := quota.Mask(resourceQuota.Status.Hard, exceeded) + return nil, admission.NewForbidden(a, + fmt.Errorf("exceeded quota: %s, requested: %s, used: %s, limited: %s", + resourceQuota.Name, + prettyPrint(failedRequestedUsage), + prettyPrint(failedUsed), + prettyPrint(failedHard))) + } + + // update to the new usage number + outQuotas[index].Status.Used = newUsage + } + + return outQuotas, nil +} + +func getScopeSelectorsFromQuota(quota corev1.ResourceQuota) []corev1.ScopedResourceSelectorRequirement { + selectors := []corev1.ScopedResourceSelectorRequirement{} + for _, scope := range quota.Spec.Scopes { + selectors = append(selectors, corev1.ScopedResourceSelectorRequirement{ + ScopeName: scope, + Operator: corev1.ScopeSelectorOpExists}) + } + if quota.Spec.ScopeSelector != nil { + for _, scopeSelector := range quota.Spec.ScopeSelector.MatchExpressions { + selectors = append(selectors, scopeSelector) + } + } + return selectors +} + +func (e *quotaEvaluator) Evaluate(a admission.Attributes) error { + e.init.Do(func() { + go e.run() + }) + + // is this resource ignored? + gvr := a.GetResource() + gr := gvr.GroupResource() + if _, ok := e.ignoredResources[gr]; ok { + return nil + } + + // if we do not know how to evaluate use for this resource, create an evaluator + evaluator := e.registry.Get(gr) + if evaluator == nil { + // create an object count evaluator if no evaluator previously registered + // note, we do not need aggregate usage here, so we pass a nil informer func + evaluator = generic.NewObjectCountEvaluator(gr, nil, "") + e.registry.Add(evaluator) + klog.Infof("quota admission added evaluator for: %s", gr) + } + // for this kind, check if the operation could mutate any quota resources + // if no resources tracked by quota are impacted, then just return + if !evaluator.Handles(a) { + return nil + } + waiter := newAdmissionWaiter(a) + + e.addWork(waiter) + + // wait for completion or timeout + select { + case <-waiter.finished: + case <-time.After(10 * time.Second): + return apierrors.NewInternalError(fmt.Errorf("resource quota evaluates timeout")) + } + + return waiter.result +} + +func (e *quotaEvaluator) addWork(a *admissionWaiter) { + e.workLock.Lock() + defer e.workLock.Unlock() + + ns := a.attributes.GetNamespace() + // this Add can trigger a Get BEFORE the work is added to a list, but this is ok because the getWork routine + // waits the worklock before retrieving the work to do, so the writes in this method will be observed + e.queue.Add(ns) + + if e.inProgress.Has(ns) { + e.dirtyWork[ns] = append(e.dirtyWork[ns], a) + return + } + + e.work[ns] = append(e.work[ns], a) +} + +func (e *quotaEvaluator) completeWork(ns string) { + e.workLock.Lock() + defer e.workLock.Unlock() + + e.queue.Done(ns) + e.work[ns] = e.dirtyWork[ns] + delete(e.dirtyWork, ns) + e.inProgress.Delete(ns) +} + +// getWork returns a namespace, a list of work items in that +// namespace, and a shutdown boolean. If not shutdown then the return +// must eventually be followed by a call on completeWork for the +// returned namespace (regardless of whether the work item list is +// empty). +func (e *quotaEvaluator) getWork() (string, []*admissionWaiter, bool) { + uncastNS, shutdown := e.queue.Get() + if shutdown { + return "", []*admissionWaiter{}, shutdown + } + ns := uncastNS.(string) + + e.workLock.Lock() + defer e.workLock.Unlock() + // at this point, we know we have a coherent view of e.work. It is entirely possible + // that our workqueue has another item requeued to it, but we'll pick it up early. This ok + // because the next time will go into our dirty list + + work := e.work[ns] + delete(e.work, ns) + delete(e.dirtyWork, ns) + e.inProgress.Insert(ns) + return ns, work, false +} + +// prettyPrint formats a resource list for usage in errors +// it outputs resources sorted in increasing order +func prettyPrint(item corev1.ResourceList) string { + parts := []string{} + keys := []string{} + for key := range item { + keys = append(keys, string(key)) + } + sort.Strings(keys) + for _, key := range keys { + value := item[corev1.ResourceName(key)] + constraint := key + "=" + value.String() + parts = append(parts, constraint) + } + return strings.Join(parts, ",") +} + +func prettyPrintResourceNames(a []corev1.ResourceName) string { + values := []string{} + for _, value := range a { + values = append(values, string(value)) + } + sort.Strings(values) + return strings.Join(values, ",") +} + +// hasUsageStats returns true if for each hard constraint in interestingResources there is a value for its current usage +func hasUsageStats(resourceQuota *corev1.ResourceQuota, interestingResources []corev1.ResourceName) bool { + interestingSet := quota.ToSet(interestingResources) + for resourceName := range resourceQuota.Status.Hard { + if !interestingSet.Has(string(resourceName)) { + continue + } + if _, found := resourceQuota.Status.Used[resourceName]; !found { + return false + } + } + return true +} diff --git a/kube/plugin/pkg/admission/resourcequota/resource_access.go b/kube/plugin/pkg/admission/resourcequota/resource_access.go new file mode 100644 index 000000000..4297f7533 --- /dev/null +++ b/kube/plugin/pkg/admission/resourcequota/resource_access.go @@ -0,0 +1,155 @@ +/* +Copyright 2016 The Kubernetes 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. +*/ + +package resourcequota + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/golang-lru" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apiserver/pkg/storage/etcd3" + "k8s.io/client-go/kubernetes" + corev1listers "k8s.io/client-go/listers/core/v1" +) + +// QuotaAccessor abstracts the get/set logic from the rest of the Evaluator. This could be a test stub, a straight passthrough, +// or most commonly a series of deconflicting caches. +type QuotaAccessor interface { + // UpdateQuotaStatus is called to persist final status. This method should write to persistent storage. + // An error indicates that write didn't complete successfully. + UpdateQuotaStatus(newQuota *corev1.ResourceQuota) error + + // GetQuotas gets all possible quotas for a given namespace + GetQuotas(namespace string) ([]corev1.ResourceQuota, error) +} + +type quotaAccessor struct { + client kubernetes.Interface + + // lister can list/get quota objects from a shared informer's cache + lister corev1listers.ResourceQuotaLister + + // liveLookups holds the last few live lookups we've done to help ammortize cost on repeated lookup failures. + // This lets us handle the case of latent caches, by looking up actual results for a namespace on cache miss/no results. + // We track the lookup result here so that for repeated requests, we don't look it up very often. + liveLookupCache *lru.Cache + liveTTL time.Duration + // updatedQuotas holds a cache of quotas that we've updated. This is used to pull the "really latest" during back to + // back quota evaluations that touch the same quota doc. This only works because we can compare etcd resourceVersions + // for the same resource as integers. Before this change: 22 updates with 12 conflicts. after this change: 15 updates with 0 conflicts + updatedQuotas *lru.Cache +} + +// newQuotaAccessor creates an object that conforms to the QuotaAccessor interface to be used to retrieve quota objects. +func newQuotaAccessor() (*quotaAccessor, error) { + liveLookupCache, err := lru.New(100) + if err != nil { + return nil, err + } + updatedCache, err := lru.New(100) + if err != nil { + return nil, err + } + + // client and lister will be set when SetInternalKubeClientSet and SetInternalKubeInformerFactory are invoked + return "aAccessor{ + liveLookupCache: liveLookupCache, + liveTTL: time.Duration(30 * time.Second), + updatedQuotas: updatedCache, + }, nil +} + +func (e *quotaAccessor) UpdateQuotaStatus(newQuota *corev1.ResourceQuota) error { + updatedQuota, err := e.client.CoreV1().ResourceQuotas(newQuota.Namespace).UpdateStatus(context.TODO(),newQuota,metav1.UpdateOptions{}) + if err != nil { + return err + } + + key := newQuota.Namespace + "/" + newQuota.Name + e.updatedQuotas.Add(key, updatedQuota) + return nil +} + +var etcdVersioner = etcd3.APIObjectVersioner{} + +// checkCache compares the passed quota against the value in the look-aside cache and returns the newer +// if the cache is out of date, it deletes the stale entry. This only works because of etcd resourceVersions +// being monotonically increasing integers +func (e *quotaAccessor) checkCache(quota *corev1.ResourceQuota) *corev1.ResourceQuota { + key := quota.Namespace + "/" + quota.Name + uncastCachedQuota, ok := e.updatedQuotas.Get(key) + if !ok { + return quota + } + cachedQuota := uncastCachedQuota.(*corev1.ResourceQuota) + + if etcdVersioner.CompareResourceVersion(quota, cachedQuota) >= 0 { + e.updatedQuotas.Remove(key) + return quota + } + return cachedQuota +} + +func (e *quotaAccessor) GetQuotas(namespace string) ([]corev1.ResourceQuota, error) { + // determine if there are any quotas in this namespace + // if there are no quotas, we don't need to do anything + items, err := e.lister.ResourceQuotas(namespace).List(labels.Everything()) + if err != nil { + return nil, fmt.Errorf("error resolving quota: %v", err) + } + + // if there are no items held in our indexer, check our live-lookup LRU, if that misses, do the live lookup to prime it. + if len(items) == 0 { + lruItemObj, ok := e.liveLookupCache.Get(namespace) + if !ok || lruItemObj.(liveLookupEntry).expiry.Before(time.Now()) { + // TODO: If there are multiple operations at the same time and cache has just expired, + // this may cause multiple List operations being issued at the same time. + // If there is already in-flight List() for a given namespace, we should wait until + // it is finished and cache is updated instead of doing the same, also to avoid + // throttling - see #22422 for details. + liveList, err := e.client.CoreV1().ResourceQuotas(namespace).List(context.TODO(),metav1.ListOptions{}) + if err != nil { + return nil, err + } + newEntry := liveLookupEntry{expiry: time.Now().Add(e.liveTTL)} + for i := range liveList.Items { + newEntry.items = append(newEntry.items, &liveList.Items[i]) + } + e.liveLookupCache.Add(namespace, newEntry) + lruItemObj = newEntry + } + lruEntry := lruItemObj.(liveLookupEntry) + for i := range lruEntry.items { + items = append(items, lruEntry.items[i]) + } + } + + resourceQuotas := []corev1.ResourceQuota{} + for i := range items { + quota := items[i] + quota = e.checkCache(quota) + // always make a copy. We're going to muck around with this and we should never mutate the originals + resourceQuotas = append(resourceQuotas, *quota) + } + + return resourceQuotas, nil +} diff --git a/pkg/apis/addtoscheme_quota_v1alpha2.go b/pkg/apis/addtoscheme_quota_v1alpha2.go new file mode 100644 index 000000000..fd3c3fd41 --- /dev/null +++ b/pkg/apis/addtoscheme_quota_v1alpha2.go @@ -0,0 +1,26 @@ +/* +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. +*/ + +package apis + +import ( + quotav1alpha2 "kubesphere.io/kubesphere/pkg/apis/quota/v1alpha2" +) + +func init() { + // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back + AddToSchemes = append(AddToSchemes, quotav1alpha2.SchemeBuilder.AddToScheme) +} diff --git a/pkg/apis/quota/group.go b/pkg/apis/quota/group.go new file mode 100644 index 000000000..0d868898f --- /dev/null +++ b/pkg/apis/quota/group.go @@ -0,0 +1,18 @@ +/* +Copyright 2020 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. +*/ + +// Package quota contains quota API versions +package quota diff --git a/pkg/apis/quota/v1alpha2/doc.go b/pkg/apis/quota/v1alpha2/doc.go new file mode 100644 index 000000000..ca399d3b8 --- /dev/null +++ b/pkg/apis/quota/v1alpha2/doc.go @@ -0,0 +1,23 @@ +/* +Copyright 2020 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. +*/ + +// Package v1alpha2 contains API Schema definitions for the quotas v1alpha2 API group +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen=package,register +// +k8s:conversion-gen=kubesphere.io/kubesphere/pkg/apis/quota +// +k8s:defaulter-gen=TypeMeta +// +groupName=quota.kubesphere.io +package v1alpha2 diff --git a/pkg/apis/quota/v1alpha2/register.go b/pkg/apis/quota/v1alpha2/register.go new file mode 100644 index 000000000..d9dd4ce33 --- /dev/null +++ b/pkg/apis/quota/v1alpha2/register.go @@ -0,0 +1,46 @@ +/* +Copyright 2020 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. +*/ + +// NOTE: Boilerplate only. Ignore this file. + +// Package v1alpha2 contains API Schema definitions for the quotas v1alpha2 API group +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen=package,register +// +k8s:conversion-gen=kubesphere.io/kubesphere/pkg/apis/quota +// +k8s:defaulter-gen=TypeMeta +// +groupName=quota.kubesphere.io +package v1alpha2 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // SchemeGroupVersion is group version used to register these objects + SchemeGroupVersion = schema.GroupVersion{Group: "quota.kubesphere.io", Version: "v1alpha2"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} + + // AddToScheme is required by pkg/client/... + AddToScheme = SchemeBuilder.AddToScheme +) + +// Resource is required by pkg/client/listers/... +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} diff --git a/pkg/apis/quota/v1alpha2/types.go b/pkg/apis/quota/v1alpha2/types.go new file mode 100644 index 000000000..2b34c590f --- /dev/null +++ b/pkg/apis/quota/v1alpha2/types.go @@ -0,0 +1,99 @@ +/* +Copyright 2020 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. +*/ + +package v1alpha2 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + ResourceKindCluster = "ResourceQuota" + ResourcesSingularCluster = "resourcequota" + ResourcesPluralCluster = "resourcequotas" +) + +func init() { + SchemeBuilder.Register(&ResourceQuota{}, &ResourceQuotaList{}) +} + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:openapi-gen=true + +// WorkspaceResourceQuota sets aggregate quota restrictions enforced per workspace +// +kubebuilder:resource:categories="quota",scope="Cluster" +// +kubebuilder:subresource:status +type ResourceQuota struct { + metav1.TypeMeta `json:",inline"` + // Standard object's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Spec defines the desired quota + Spec ResourceQuotaSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"` + + // Status defines the actual enforced quota and its current usage + // +optional + Status ResourceQuotaStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// ResourceQuotaSpec defines the desired quota restrictions +type ResourceQuotaSpec struct { + // LabelSelector is used to select projects by label. + LabelSelector map[string]string `json:"selector" protobuf:"bytes,1,opt,name=selector"` + + // Quota defines the desired quota + Quota corev1.ResourceQuotaSpec `json:"quota" protobuf:"bytes,2,opt,name=quota"` +} + +// ResourceQuotaStatus defines the actual enforced quota and its current usage +type ResourceQuotaStatus struct { + // Total defines the actual enforced quota and its current usage across all projects + Total corev1.ResourceQuotaStatus `json:"total" protobuf:"bytes,1,opt,name=total"` + + // Namespaces slices the usage by project. + Namespaces ResourceQuotasStatusByNamespace `json:"namespaces" protobuf:"bytes,2,rep,name=namespaces"` +} + +// ResourceQuotasStatusByNamespace bundles multiple ResourceQuotaStatusByNamespace +type ResourceQuotasStatusByNamespace []ResourceQuotaStatusByNamespace + +// ResourceQuotaStatusByNamespace gives status for a particular project +type ResourceQuotaStatusByNamespace struct { + corev1.ResourceQuotaStatus `json:",inline"` + + // Namespace the project this status applies to + Namespace string `json:"namespace" protobuf:"bytes,1,opt,name=namespace"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ResourceQuotaList is a list of WorkspaceResourceQuota items. +type ResourceQuotaList struct { + metav1.TypeMeta `json:",inline"` + // Standard list metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Items is a list of WorkspaceResourceQuota objects. + // More info: https://kubernetes.io/docs/concepts/policy/resource-quotas/ + Items []ResourceQuota `json:"items" protobuf:"bytes,2,rep,name=items"` +} diff --git a/pkg/apis/quota/v1alpha2/types_test.go b/pkg/apis/quota/v1alpha2/types_test.go new file mode 100644 index 000000000..356957c18 --- /dev/null +++ b/pkg/apis/quota/v1alpha2/types_test.go @@ -0,0 +1,64 @@ +/* +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. +*/ + +package v1alpha2 + +import ( + "testing" + + "github.com/onsi/gomega" + "golang.org/x/net/context" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func TestStorageResourceQuota(t *testing.T) { + key := types.NamespacedName{ + Name: "foo", + } + created := &ResourceQuota{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: ResourceQuotaSpec{ + LabelSelector: map[string]string{}, + }, + } + g := gomega.NewGomegaWithT(t) + + // Test Create + fetched := &ResourceQuota{ + Spec: ResourceQuotaSpec{ + LabelSelector: map[string]string{}, + }, + } + g.Expect(c.Create(context.TODO(), created)).To(gomega.Succeed()) + + g.Expect(c.Get(context.TODO(), key, fetched)).To(gomega.Succeed()) + g.Expect(fetched).To(gomega.Equal(created)) + + // Test Updating the Labels + updated := fetched.DeepCopy() + updated.Labels = map[string]string{"hello": "world"} + g.Expect(c.Update(context.TODO(), updated)).To(gomega.Succeed()) + + g.Expect(c.Get(context.TODO(), key, fetched)).To(gomega.Succeed()) + g.Expect(fetched).To(gomega.Equal(updated)) + + // Test Delete + g.Expect(c.Delete(context.TODO(), fetched)).To(gomega.Succeed()) + g.Expect(c.Get(context.TODO(), key, fetched)).ToNot(gomega.Succeed()) +} diff --git a/pkg/apis/quota/v1alpha2/v1alpha2_suite_test.go b/pkg/apis/quota/v1alpha2/v1alpha2_suite_test.go new file mode 100644 index 000000000..717d9ce14 --- /dev/null +++ b/pkg/apis/quota/v1alpha2/v1alpha2_suite_test.go @@ -0,0 +1,55 @@ +/* +Copyright 2020 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. +*/ + +package v1alpha2 + +import ( + "log" + "os" + "path/filepath" + "testing" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" +) + +var cfg *rest.Config +var c client.Client + +func TestMain(m *testing.M) { + t := &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "config", "crds")}, + } + + err := SchemeBuilder.AddToScheme(scheme.Scheme) + if err != nil { + log.Fatal(err) + } + + if cfg, err = t.Start(); err != nil { + log.Fatal(err) + } + + if c, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}); err != nil { + log.Fatal(err) + } + + code := m.Run() + t.Stop() + os.Exit(code) +} diff --git a/pkg/apis/quota/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/quota/v1alpha2/zz_generated.deepcopy.go new file mode 100644 index 000000000..5be7a8ab7 --- /dev/null +++ b/pkg/apis/quota/v1alpha2/zz_generated.deepcopy.go @@ -0,0 +1,167 @@ +// +build !ignore_autogenerated + +/* +Copyright 2020 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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceQuota) DeepCopyInto(out *ResourceQuota) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceQuota. +func (in *ResourceQuota) DeepCopy() *ResourceQuota { + if in == nil { + return nil + } + out := new(ResourceQuota) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ResourceQuota) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceQuotaList) DeepCopyInto(out *ResourceQuotaList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ResourceQuota, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceQuotaList. +func (in *ResourceQuotaList) DeepCopy() *ResourceQuotaList { + if in == nil { + return nil + } + out := new(ResourceQuotaList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ResourceQuotaList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceQuotaSpec) DeepCopyInto(out *ResourceQuotaSpec) { + *out = *in + if in.LabelSelector != nil { + in, out := &in.LabelSelector, &out.LabelSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.Quota.DeepCopyInto(&out.Quota) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceQuotaSpec. +func (in *ResourceQuotaSpec) DeepCopy() *ResourceQuotaSpec { + if in == nil { + return nil + } + out := new(ResourceQuotaSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceQuotaStatus) DeepCopyInto(out *ResourceQuotaStatus) { + *out = *in + in.Total.DeepCopyInto(&out.Total) + if in.Namespaces != nil { + in, out := &in.Namespaces, &out.Namespaces + *out = make(ResourceQuotasStatusByNamespace, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceQuotaStatus. +func (in *ResourceQuotaStatus) DeepCopy() *ResourceQuotaStatus { + if in == nil { + return nil + } + out := new(ResourceQuotaStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceQuotaStatusByNamespace) DeepCopyInto(out *ResourceQuotaStatusByNamespace) { + *out = *in + in.ResourceQuotaStatus.DeepCopyInto(&out.ResourceQuotaStatus) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceQuotaStatusByNamespace. +func (in *ResourceQuotaStatusByNamespace) DeepCopy() *ResourceQuotaStatusByNamespace { + if in == nil { + return nil + } + out := new(ResourceQuotaStatusByNamespace) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ResourceQuotasStatusByNamespace) DeepCopyInto(out *ResourceQuotasStatusByNamespace) { + { + in := &in + *out = make(ResourceQuotasStatusByNamespace, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceQuotasStatusByNamespace. +func (in ResourceQuotasStatusByNamespace) DeepCopy() ResourceQuotasStatusByNamespace { + if in == nil { + return nil + } + out := new(ResourceQuotasStatusByNamespace) + in.DeepCopyInto(out) + return *out +} diff --git a/pkg/client/clientset/versioned/clientset.go b/pkg/client/clientset/versioned/clientset.go index d06040f09..6537fe1df 100644 --- a/pkg/client/clientset/versioned/clientset.go +++ b/pkg/client/clientset/versioned/clientset.go @@ -30,6 +30,7 @@ import ( devopsv1alpha3 "kubesphere.io/kubesphere/pkg/client/clientset/versioned/typed/devops/v1alpha3" iamv1alpha2 "kubesphere.io/kubesphere/pkg/client/clientset/versioned/typed/iam/v1alpha2" networkv1alpha1 "kubesphere.io/kubesphere/pkg/client/clientset/versioned/typed/network/v1alpha1" + quotav1alpha2 "kubesphere.io/kubesphere/pkg/client/clientset/versioned/typed/quota/v1alpha2" servicemeshv1alpha2 "kubesphere.io/kubesphere/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2" storagev1alpha1 "kubesphere.io/kubesphere/pkg/client/clientset/versioned/typed/storage/v1alpha1" tenantv1alpha1 "kubesphere.io/kubesphere/pkg/client/clientset/versioned/typed/tenant/v1alpha1" @@ -45,6 +46,7 @@ type Interface interface { DevopsV1alpha3() devopsv1alpha3.DevopsV1alpha3Interface IamV1alpha2() iamv1alpha2.IamV1alpha2Interface NetworkV1alpha1() networkv1alpha1.NetworkV1alpha1Interface + QuotaV1alpha2() quotav1alpha2.QuotaV1alpha2Interface ServicemeshV1alpha2() servicemeshv1alpha2.ServicemeshV1alpha2Interface StorageV1alpha1() storagev1alpha1.StorageV1alpha1Interface TenantV1alpha1() tenantv1alpha1.TenantV1alpha1Interface @@ -62,6 +64,7 @@ type Clientset struct { devopsV1alpha3 *devopsv1alpha3.DevopsV1alpha3Client iamV1alpha2 *iamv1alpha2.IamV1alpha2Client networkV1alpha1 *networkv1alpha1.NetworkV1alpha1Client + quotaV1alpha2 *quotav1alpha2.QuotaV1alpha2Client servicemeshV1alpha2 *servicemeshv1alpha2.ServicemeshV1alpha2Client storageV1alpha1 *storagev1alpha1.StorageV1alpha1Client tenantV1alpha1 *tenantv1alpha1.TenantV1alpha1Client @@ -99,6 +102,11 @@ func (c *Clientset) NetworkV1alpha1() networkv1alpha1.NetworkV1alpha1Interface { return c.networkV1alpha1 } +// QuotaV1alpha2 retrieves the QuotaV1alpha2Client +func (c *Clientset) QuotaV1alpha2() quotav1alpha2.QuotaV1alpha2Interface { + return c.quotaV1alpha2 +} + // ServicemeshV1alpha2 retrieves the ServicemeshV1alpha2Client func (c *Clientset) ServicemeshV1alpha2() servicemeshv1alpha2.ServicemeshV1alpha2Interface { return c.servicemeshV1alpha2 @@ -169,6 +177,10 @@ func NewForConfig(c *rest.Config) (*Clientset, error) { if err != nil { return nil, err } + cs.quotaV1alpha2, err = quotav1alpha2.NewForConfig(&configShallowCopy) + if err != nil { + return nil, err + } cs.servicemeshV1alpha2, err = servicemeshv1alpha2.NewForConfig(&configShallowCopy) if err != nil { return nil, err @@ -207,6 +219,7 @@ func NewForConfigOrDie(c *rest.Config) *Clientset { cs.devopsV1alpha3 = devopsv1alpha3.NewForConfigOrDie(c) cs.iamV1alpha2 = iamv1alpha2.NewForConfigOrDie(c) cs.networkV1alpha1 = networkv1alpha1.NewForConfigOrDie(c) + cs.quotaV1alpha2 = quotav1alpha2.NewForConfigOrDie(c) cs.servicemeshV1alpha2 = servicemeshv1alpha2.NewForConfigOrDie(c) cs.storageV1alpha1 = storagev1alpha1.NewForConfigOrDie(c) cs.tenantV1alpha1 = tenantv1alpha1.NewForConfigOrDie(c) @@ -226,6 +239,7 @@ func New(c rest.Interface) *Clientset { cs.devopsV1alpha3 = devopsv1alpha3.New(c) cs.iamV1alpha2 = iamv1alpha2.New(c) cs.networkV1alpha1 = networkv1alpha1.New(c) + cs.quotaV1alpha2 = quotav1alpha2.New(c) cs.servicemeshV1alpha2 = servicemeshv1alpha2.New(c) cs.storageV1alpha1 = storagev1alpha1.New(c) cs.tenantV1alpha1 = tenantv1alpha1.New(c) diff --git a/pkg/client/clientset/versioned/fake/clientset_generated.go b/pkg/client/clientset/versioned/fake/clientset_generated.go index 8a8f43774..b13a4bd00 100644 --- a/pkg/client/clientset/versioned/fake/clientset_generated.go +++ b/pkg/client/clientset/versioned/fake/clientset_generated.go @@ -37,6 +37,8 @@ import ( fakeiamv1alpha2 "kubesphere.io/kubesphere/pkg/client/clientset/versioned/typed/iam/v1alpha2/fake" networkv1alpha1 "kubesphere.io/kubesphere/pkg/client/clientset/versioned/typed/network/v1alpha1" fakenetworkv1alpha1 "kubesphere.io/kubesphere/pkg/client/clientset/versioned/typed/network/v1alpha1/fake" + quotav1alpha2 "kubesphere.io/kubesphere/pkg/client/clientset/versioned/typed/quota/v1alpha2" + fakequotav1alpha2 "kubesphere.io/kubesphere/pkg/client/clientset/versioned/typed/quota/v1alpha2/fake" servicemeshv1alpha2 "kubesphere.io/kubesphere/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2" fakeservicemeshv1alpha2 "kubesphere.io/kubesphere/pkg/client/clientset/versioned/typed/servicemesh/v1alpha2/fake" storagev1alpha1 "kubesphere.io/kubesphere/pkg/client/clientset/versioned/typed/storage/v1alpha1" @@ -126,6 +128,11 @@ func (c *Clientset) NetworkV1alpha1() networkv1alpha1.NetworkV1alpha1Interface { return &fakenetworkv1alpha1.FakeNetworkV1alpha1{Fake: &c.Fake} } +// QuotaV1alpha2 retrieves the QuotaV1alpha2Client +func (c *Clientset) QuotaV1alpha2() quotav1alpha2.QuotaV1alpha2Interface { + return &fakequotav1alpha2.FakeQuotaV1alpha2{Fake: &c.Fake} +} + // ServicemeshV1alpha2 retrieves the ServicemeshV1alpha2Client func (c *Clientset) ServicemeshV1alpha2() servicemeshv1alpha2.ServicemeshV1alpha2Interface { return &fakeservicemeshv1alpha2.FakeServicemeshV1alpha2{Fake: &c.Fake} diff --git a/pkg/client/clientset/versioned/fake/register.go b/pkg/client/clientset/versioned/fake/register.go index f749e88d6..eb9569f56 100644 --- a/pkg/client/clientset/versioned/fake/register.go +++ b/pkg/client/clientset/versioned/fake/register.go @@ -30,6 +30,7 @@ import ( devopsv1alpha3 "kubesphere.io/kubesphere/pkg/apis/devops/v1alpha3" iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2" networkv1alpha1 "kubesphere.io/kubesphere/pkg/apis/network/v1alpha1" + quotav1alpha2 "kubesphere.io/kubesphere/pkg/apis/quota/v1alpha2" servicemeshv1alpha2 "kubesphere.io/kubesphere/pkg/apis/servicemesh/v1alpha2" storagev1alpha1 "kubesphere.io/kubesphere/pkg/apis/storage/v1alpha1" tenantv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1" @@ -47,6 +48,7 @@ var localSchemeBuilder = runtime.SchemeBuilder{ devopsv1alpha3.AddToScheme, iamv1alpha2.AddToScheme, networkv1alpha1.AddToScheme, + quotav1alpha2.AddToScheme, servicemeshv1alpha2.AddToScheme, storagev1alpha1.AddToScheme, tenantv1alpha1.AddToScheme, diff --git a/pkg/client/clientset/versioned/scheme/register.go b/pkg/client/clientset/versioned/scheme/register.go index c0b5d6837..28002f8f3 100644 --- a/pkg/client/clientset/versioned/scheme/register.go +++ b/pkg/client/clientset/versioned/scheme/register.go @@ -30,6 +30,7 @@ import ( devopsv1alpha3 "kubesphere.io/kubesphere/pkg/apis/devops/v1alpha3" iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2" networkv1alpha1 "kubesphere.io/kubesphere/pkg/apis/network/v1alpha1" + quotav1alpha2 "kubesphere.io/kubesphere/pkg/apis/quota/v1alpha2" servicemeshv1alpha2 "kubesphere.io/kubesphere/pkg/apis/servicemesh/v1alpha2" storagev1alpha1 "kubesphere.io/kubesphere/pkg/apis/storage/v1alpha1" tenantv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1" @@ -47,6 +48,7 @@ var localSchemeBuilder = runtime.SchemeBuilder{ devopsv1alpha3.AddToScheme, iamv1alpha2.AddToScheme, networkv1alpha1.AddToScheme, + quotav1alpha2.AddToScheme, servicemeshv1alpha2.AddToScheme, storagev1alpha1.AddToScheme, tenantv1alpha1.AddToScheme, diff --git a/pkg/client/clientset/versioned/typed/quota/v1alpha2/doc.go b/pkg/client/clientset/versioned/typed/quota/v1alpha2/doc.go new file mode 100644 index 000000000..f6328297b --- /dev/null +++ b/pkg/client/clientset/versioned/typed/quota/v1alpha2/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2020 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha2 diff --git a/pkg/client/clientset/versioned/typed/quota/v1alpha2/fake/doc.go b/pkg/client/clientset/versioned/typed/quota/v1alpha2/fake/doc.go new file mode 100644 index 000000000..7e36dbca8 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/quota/v1alpha2/fake/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2020 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/pkg/client/clientset/versioned/typed/quota/v1alpha2/fake/fake_quota_client.go b/pkg/client/clientset/versioned/typed/quota/v1alpha2/fake/fake_quota_client.go new file mode 100644 index 000000000..e6b0350f9 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/quota/v1alpha2/fake/fake_quota_client.go @@ -0,0 +1,40 @@ +/* +Copyright 2020 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" + v1alpha2 "kubesphere.io/kubesphere/pkg/client/clientset/versioned/typed/quota/v1alpha2" +) + +type FakeQuotaV1alpha2 struct { + *testing.Fake +} + +func (c *FakeQuotaV1alpha2) ResourceQuotas() v1alpha2.ResourceQuotaInterface { + return &FakeResourceQuotas{c} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeQuotaV1alpha2) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/pkg/client/clientset/versioned/typed/quota/v1alpha2/fake/fake_resourcequota.go b/pkg/client/clientset/versioned/typed/quota/v1alpha2/fake/fake_resourcequota.go new file mode 100644 index 000000000..9e5f91b8d --- /dev/null +++ b/pkg/client/clientset/versioned/typed/quota/v1alpha2/fake/fake_resourcequota.go @@ -0,0 +1,133 @@ +/* +Copyright 2020 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" + v1alpha2 "kubesphere.io/kubesphere/pkg/apis/quota/v1alpha2" +) + +// FakeResourceQuotas implements ResourceQuotaInterface +type FakeResourceQuotas struct { + Fake *FakeQuotaV1alpha2 +} + +var resourcequotasResource = schema.GroupVersionResource{Group: "quota.kubesphere.io", Version: "v1alpha2", Resource: "resourcequotas"} + +var resourcequotasKind = schema.GroupVersionKind{Group: "quota.kubesphere.io", Version: "v1alpha2", Kind: "ResourceQuota"} + +// Get takes name of the resourceQuota, and returns the corresponding resourceQuota object, and an error if there is any. +func (c *FakeResourceQuotas) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha2.ResourceQuota, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootGetAction(resourcequotasResource, name), &v1alpha2.ResourceQuota{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.ResourceQuota), err +} + +// List takes label and field selectors, and returns the list of ResourceQuotas that match those selectors. +func (c *FakeResourceQuotas) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha2.ResourceQuotaList, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootListAction(resourcequotasResource, resourcequotasKind, opts), &v1alpha2.ResourceQuotaList{}) + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha2.ResourceQuotaList{ListMeta: obj.(*v1alpha2.ResourceQuotaList).ListMeta} + for _, item := range obj.(*v1alpha2.ResourceQuotaList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested resourceQuotas. +func (c *FakeResourceQuotas) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewRootWatchAction(resourcequotasResource, opts)) +} + +// Create takes the representation of a resourceQuota and creates it. Returns the server's representation of the resourceQuota, and an error, if there is any. +func (c *FakeResourceQuotas) Create(ctx context.Context, resourceQuota *v1alpha2.ResourceQuota, opts v1.CreateOptions) (result *v1alpha2.ResourceQuota, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootCreateAction(resourcequotasResource, resourceQuota), &v1alpha2.ResourceQuota{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.ResourceQuota), err +} + +// Update takes the representation of a resourceQuota and updates it. Returns the server's representation of the resourceQuota, and an error, if there is any. +func (c *FakeResourceQuotas) Update(ctx context.Context, resourceQuota *v1alpha2.ResourceQuota, opts v1.UpdateOptions) (result *v1alpha2.ResourceQuota, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootUpdateAction(resourcequotasResource, resourceQuota), &v1alpha2.ResourceQuota{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.ResourceQuota), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeResourceQuotas) UpdateStatus(ctx context.Context, resourceQuota *v1alpha2.ResourceQuota, opts v1.UpdateOptions) (*v1alpha2.ResourceQuota, error) { + obj, err := c.Fake. + Invokes(testing.NewRootUpdateSubresourceAction(resourcequotasResource, "status", resourceQuota), &v1alpha2.ResourceQuota{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.ResourceQuota), err +} + +// Delete takes name of the resourceQuota and deletes it. Returns an error if one occurs. +func (c *FakeResourceQuotas) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewRootDeleteAction(resourcequotasResource, name), &v1alpha2.ResourceQuota{}) + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeResourceQuotas) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewRootDeleteCollectionAction(resourcequotasResource, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha2.ResourceQuotaList{}) + return err +} + +// Patch applies the patch and returns the patched resourceQuota. +func (c *FakeResourceQuotas) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha2.ResourceQuota, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootPatchSubresourceAction(resourcequotasResource, name, pt, data, subresources...), &v1alpha2.ResourceQuota{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.ResourceQuota), err +} diff --git a/pkg/client/clientset/versioned/typed/quota/v1alpha2/generated_expansion.go b/pkg/client/clientset/versioned/typed/quota/v1alpha2/generated_expansion.go new file mode 100644 index 000000000..9dd00cbe2 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/quota/v1alpha2/generated_expansion.go @@ -0,0 +1,21 @@ +/* +Copyright 2020 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha2 + +type ResourceQuotaExpansion interface{} diff --git a/pkg/client/clientset/versioned/typed/quota/v1alpha2/quota_client.go b/pkg/client/clientset/versioned/typed/quota/v1alpha2/quota_client.go new file mode 100644 index 000000000..1f4cb15c6 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/quota/v1alpha2/quota_client.go @@ -0,0 +1,89 @@ +/* +Copyright 2020 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + rest "k8s.io/client-go/rest" + v1alpha2 "kubesphere.io/kubesphere/pkg/apis/quota/v1alpha2" + "kubesphere.io/kubesphere/pkg/client/clientset/versioned/scheme" +) + +type QuotaV1alpha2Interface interface { + RESTClient() rest.Interface + ResourceQuotasGetter +} + +// QuotaV1alpha2Client is used to interact with features provided by the quota.kubesphere.io group. +type QuotaV1alpha2Client struct { + restClient rest.Interface +} + +func (c *QuotaV1alpha2Client) ResourceQuotas() ResourceQuotaInterface { + return newResourceQuotas(c) +} + +// NewForConfig creates a new QuotaV1alpha2Client for the given config. +func NewForConfig(c *rest.Config) (*QuotaV1alpha2Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientFor(&config) + if err != nil { + return nil, err + } + return &QuotaV1alpha2Client{client}, nil +} + +// NewForConfigOrDie creates a new QuotaV1alpha2Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *QuotaV1alpha2Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new QuotaV1alpha2Client for the given RESTClient. +func New(c rest.Interface) *QuotaV1alpha2Client { + return &QuotaV1alpha2Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1alpha2.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *QuotaV1alpha2Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/pkg/client/clientset/versioned/typed/quota/v1alpha2/resourcequota.go b/pkg/client/clientset/versioned/typed/quota/v1alpha2/resourcequota.go new file mode 100644 index 000000000..7a631d405 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/quota/v1alpha2/resourcequota.go @@ -0,0 +1,184 @@ +/* +Copyright 2020 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + "context" + "time" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" + v1alpha2 "kubesphere.io/kubesphere/pkg/apis/quota/v1alpha2" + scheme "kubesphere.io/kubesphere/pkg/client/clientset/versioned/scheme" +) + +// ResourceQuotasGetter has a method to return a ResourceQuotaInterface. +// A group's client should implement this interface. +type ResourceQuotasGetter interface { + ResourceQuotas() ResourceQuotaInterface +} + +// ResourceQuotaInterface has methods to work with ResourceQuota resources. +type ResourceQuotaInterface interface { + Create(ctx context.Context, resourceQuota *v1alpha2.ResourceQuota, opts v1.CreateOptions) (*v1alpha2.ResourceQuota, error) + Update(ctx context.Context, resourceQuota *v1alpha2.ResourceQuota, opts v1.UpdateOptions) (*v1alpha2.ResourceQuota, error) + UpdateStatus(ctx context.Context, resourceQuota *v1alpha2.ResourceQuota, opts v1.UpdateOptions) (*v1alpha2.ResourceQuota, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha2.ResourceQuota, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha2.ResourceQuotaList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha2.ResourceQuota, err error) + ResourceQuotaExpansion +} + +// resourceQuotas implements ResourceQuotaInterface +type resourceQuotas struct { + client rest.Interface +} + +// newResourceQuotas returns a ResourceQuotas +func newResourceQuotas(c *QuotaV1alpha2Client) *resourceQuotas { + return &resourceQuotas{ + client: c.RESTClient(), + } +} + +// Get takes name of the resourceQuota, and returns the corresponding resourceQuota object, and an error if there is any. +func (c *resourceQuotas) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha2.ResourceQuota, err error) { + result = &v1alpha2.ResourceQuota{} + err = c.client.Get(). + Resource("resourcequotas"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of ResourceQuotas that match those selectors. +func (c *resourceQuotas) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha2.ResourceQuotaList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha2.ResourceQuotaList{} + err = c.client.Get(). + Resource("resourcequotas"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested resourceQuotas. +func (c *resourceQuotas) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Resource("resourcequotas"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a resourceQuota and creates it. Returns the server's representation of the resourceQuota, and an error, if there is any. +func (c *resourceQuotas) Create(ctx context.Context, resourceQuota *v1alpha2.ResourceQuota, opts v1.CreateOptions) (result *v1alpha2.ResourceQuota, err error) { + result = &v1alpha2.ResourceQuota{} + err = c.client.Post(). + Resource("resourcequotas"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(resourceQuota). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a resourceQuota and updates it. Returns the server's representation of the resourceQuota, and an error, if there is any. +func (c *resourceQuotas) Update(ctx context.Context, resourceQuota *v1alpha2.ResourceQuota, opts v1.UpdateOptions) (result *v1alpha2.ResourceQuota, err error) { + result = &v1alpha2.ResourceQuota{} + err = c.client.Put(). + Resource("resourcequotas"). + Name(resourceQuota.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(resourceQuota). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *resourceQuotas) UpdateStatus(ctx context.Context, resourceQuota *v1alpha2.ResourceQuota, opts v1.UpdateOptions) (result *v1alpha2.ResourceQuota, err error) { + result = &v1alpha2.ResourceQuota{} + err = c.client.Put(). + Resource("resourcequotas"). + Name(resourceQuota.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(resourceQuota). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the resourceQuota and deletes it. Returns an error if one occurs. +func (c *resourceQuotas) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Resource("resourcequotas"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *resourceQuotas) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Resource("resourcequotas"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched resourceQuota. +func (c *resourceQuotas) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha2.ResourceQuota, err error) { + result = &v1alpha2.ResourceQuota{} + err = c.client.Patch(pt). + Resource("resourcequotas"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/client/informers/externalversions/factory.go b/pkg/client/informers/externalversions/factory.go index dc54b8950..89e3269e6 100644 --- a/pkg/client/informers/externalversions/factory.go +++ b/pkg/client/informers/externalversions/factory.go @@ -34,6 +34,7 @@ import ( iam "kubesphere.io/kubesphere/pkg/client/informers/externalversions/iam" internalinterfaces "kubesphere.io/kubesphere/pkg/client/informers/externalversions/internalinterfaces" network "kubesphere.io/kubesphere/pkg/client/informers/externalversions/network" + quota "kubesphere.io/kubesphere/pkg/client/informers/externalversions/quota" servicemesh "kubesphere.io/kubesphere/pkg/client/informers/externalversions/servicemesh" storage "kubesphere.io/kubesphere/pkg/client/informers/externalversions/storage" tenant "kubesphere.io/kubesphere/pkg/client/informers/externalversions/tenant" @@ -185,6 +186,7 @@ type SharedInformerFactory interface { Devops() devops.Interface Iam() iam.Interface Network() network.Interface + Quota() quota.Interface Servicemesh() servicemesh.Interface Storage() storage.Interface Tenant() tenant.Interface @@ -211,6 +213,10 @@ func (f *sharedInformerFactory) Network() network.Interface { return network.New(f, f.namespace, f.tweakListOptions) } +func (f *sharedInformerFactory) Quota() quota.Interface { + return quota.New(f, f.namespace, f.tweakListOptions) +} + func (f *sharedInformerFactory) Servicemesh() servicemesh.Interface { return servicemesh.New(f, f.namespace, f.tweakListOptions) } diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index 9749baab1..02989bf5a 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -29,6 +29,7 @@ import ( v1alpha3 "kubesphere.io/kubesphere/pkg/apis/devops/v1alpha3" v1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2" networkv1alpha1 "kubesphere.io/kubesphere/pkg/apis/network/v1alpha1" + quotav1alpha2 "kubesphere.io/kubesphere/pkg/apis/quota/v1alpha2" servicemeshv1alpha2 "kubesphere.io/kubesphere/pkg/apis/servicemesh/v1alpha2" storagev1alpha1 "kubesphere.io/kubesphere/pkg/apis/storage/v1alpha1" tenantv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1" @@ -118,6 +119,10 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource case networkv1alpha1.SchemeGroupVersion.WithResource("namespacenetworkpolicies"): return &genericInformer{resource: resource.GroupResource(), informer: f.Network().V1alpha1().NamespaceNetworkPolicies().Informer()}, nil + // Group=quota.kubesphere.io, Version=v1alpha2 + case quotav1alpha2.SchemeGroupVersion.WithResource("resourcequotas"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Quota().V1alpha2().ResourceQuotas().Informer()}, nil + // Group=servicemesh.kubesphere.io, Version=v1alpha2 case servicemeshv1alpha2.SchemeGroupVersion.WithResource("servicepolicies"): return &genericInformer{resource: resource.GroupResource(), informer: f.Servicemesh().V1alpha2().ServicePolicies().Informer()}, nil diff --git a/pkg/client/informers/externalversions/quota/interface.go b/pkg/client/informers/externalversions/quota/interface.go new file mode 100644 index 000000000..2c73c5c0d --- /dev/null +++ b/pkg/client/informers/externalversions/quota/interface.go @@ -0,0 +1,46 @@ +/* +Copyright 2020 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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package quota + +import ( + internalinterfaces "kubesphere.io/kubesphere/pkg/client/informers/externalversions/internalinterfaces" + v1alpha2 "kubesphere.io/kubesphere/pkg/client/informers/externalversions/quota/v1alpha2" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V1alpha2 provides access to shared informers for resources in V1alpha2. + V1alpha2() v1alpha2.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V1alpha2 returns a new v1alpha2.Interface. +func (g *group) V1alpha2() v1alpha2.Interface { + return v1alpha2.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/pkg/client/informers/externalversions/quota/v1alpha2/interface.go b/pkg/client/informers/externalversions/quota/v1alpha2/interface.go new file mode 100644 index 000000000..065e5d00b --- /dev/null +++ b/pkg/client/informers/externalversions/quota/v1alpha2/interface.go @@ -0,0 +1,45 @@ +/* +Copyright 2020 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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + internalinterfaces "kubesphere.io/kubesphere/pkg/client/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // ResourceQuotas returns a ResourceQuotaInformer. + ResourceQuotas() ResourceQuotaInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// ResourceQuotas returns a ResourceQuotaInformer. +func (v *version) ResourceQuotas() ResourceQuotaInformer { + return &resourceQuotaInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/client/informers/externalversions/quota/v1alpha2/resourcequota.go b/pkg/client/informers/externalversions/quota/v1alpha2/resourcequota.go new file mode 100644 index 000000000..349e491ba --- /dev/null +++ b/pkg/client/informers/externalversions/quota/v1alpha2/resourcequota.go @@ -0,0 +1,89 @@ +/* +Copyright 2020 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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + "context" + time "time" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" + quotav1alpha2 "kubesphere.io/kubesphere/pkg/apis/quota/v1alpha2" + versioned "kubesphere.io/kubesphere/pkg/client/clientset/versioned" + internalinterfaces "kubesphere.io/kubesphere/pkg/client/informers/externalversions/internalinterfaces" + v1alpha2 "kubesphere.io/kubesphere/pkg/client/listers/quota/v1alpha2" +) + +// ResourceQuotaInformer provides access to a shared informer and lister for +// ResourceQuotas. +type ResourceQuotaInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha2.ResourceQuotaLister +} + +type resourceQuotaInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// NewResourceQuotaInformer constructs a new informer for ResourceQuota type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewResourceQuotaInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredResourceQuotaInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredResourceQuotaInformer constructs a new informer for ResourceQuota type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredResourceQuotaInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.QuotaV1alpha2().ResourceQuotas().List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.QuotaV1alpha2().ResourceQuotas().Watch(context.TODO(), options) + }, + }, + "av1alpha2.ResourceQuota{}, + resyncPeriod, + indexers, + ) +} + +func (f *resourceQuotaInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredResourceQuotaInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *resourceQuotaInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor("av1alpha2.ResourceQuota{}, f.defaultInformer) +} + +func (f *resourceQuotaInformer) Lister() v1alpha2.ResourceQuotaLister { + return v1alpha2.NewResourceQuotaLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/listers/quota/v1alpha2/expansion_generated.go b/pkg/client/listers/quota/v1alpha2/expansion_generated.go new file mode 100644 index 000000000..82d4a6d4f --- /dev/null +++ b/pkg/client/listers/quota/v1alpha2/expansion_generated.go @@ -0,0 +1,23 @@ +/* +Copyright 2020 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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha2 + +// ResourceQuotaListerExpansion allows custom methods to be added to +// ResourceQuotaLister. +type ResourceQuotaListerExpansion interface{} diff --git a/pkg/client/listers/quota/v1alpha2/resourcequota.go b/pkg/client/listers/quota/v1alpha2/resourcequota.go new file mode 100644 index 000000000..bae73d47f --- /dev/null +++ b/pkg/client/listers/quota/v1alpha2/resourcequota.go @@ -0,0 +1,65 @@ +/* +Copyright 2020 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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" + v1alpha2 "kubesphere.io/kubesphere/pkg/apis/quota/v1alpha2" +) + +// ResourceQuotaLister helps list ResourceQuotas. +type ResourceQuotaLister interface { + // List lists all ResourceQuotas in the indexer. + List(selector labels.Selector) (ret []*v1alpha2.ResourceQuota, err error) + // Get retrieves the ResourceQuota from the index for a given name. + Get(name string) (*v1alpha2.ResourceQuota, error) + ResourceQuotaListerExpansion +} + +// resourceQuotaLister implements the ResourceQuotaLister interface. +type resourceQuotaLister struct { + indexer cache.Indexer +} + +// NewResourceQuotaLister returns a new ResourceQuotaLister. +func NewResourceQuotaLister(indexer cache.Indexer) ResourceQuotaLister { + return &resourceQuotaLister{indexer: indexer} +} + +// List lists all ResourceQuotas in the indexer. +func (s *resourceQuotaLister) List(selector labels.Selector) (ret []*v1alpha2.ResourceQuota, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha2.ResourceQuota)) + }) + return ret, err +} + +// Get retrieves the ResourceQuota from the index for a given name. +func (s *resourceQuotaLister) Get(name string) (*v1alpha2.ResourceQuota, error) { + obj, exists, err := s.indexer.GetByKey(name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha2.Resource("resourcequota"), name) + } + return obj.(*v1alpha2.ResourceQuota), nil +} diff --git a/pkg/controller/quota/accessor.go b/pkg/controller/quota/accessor.go new file mode 100644 index 000000000..ec28161fe --- /dev/null +++ b/pkg/controller/quota/accessor.go @@ -0,0 +1,206 @@ +/* + + Copyright 2021 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. + +*/ + +package quota + +import ( + "context" + "k8s.io/apimachinery/pkg/types" + "k8s.io/klog" + quotav1alpha2 "kubesphere.io/kubesphere/pkg/apis/quota/v1alpha2" + "sigs.k8s.io/controller-runtime/pkg/client" + "time" + + lru "github.com/hashicorp/golang-lru" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + utilwait "k8s.io/apimachinery/pkg/util/wait" + etcd "k8s.io/apiserver/pkg/storage/etcd3" + utilquota "kubesphere.io/kubesphere/kube/pkg/quota/v1" +) + +// Following code copied from github.com/openshift/apiserver-library-go/pkg/admission/quota/clusterresourcequota + +type accessor struct { + client client.Client + + // updatedResourceQuotas holds a cache of quotas that we've updated. This is used to pull the "really latest" during back to + // back quota evaluations that touch the same quota doc. This only works because we can compare etcd resourceVersions + // for the same resource as integers. Before this change: 22 updates with 12 conflicts. after this change: 15 updates with 0 conflicts + updatedResourceQuotas *lru.Cache +} + +// newQuotaAccessor creates an object that conforms to the QuotaAccessor interface to be used to retrieve quota objects. +func newQuotaAccessor(client client.Client) *accessor { + updatedCache, err := lru.New(100) + if err != nil { + // this should never happen + panic(err) + } + + return &accessor{ + client: client, + updatedResourceQuotas: updatedCache, + } +} + +// UpdateQuotaStatus the newQuota coming in will be incremented from the original. The difference between the original +// and the new is the amount to add to the namespace total, but the total status is the used value itself +func (a *accessor) UpdateQuotaStatus(newQuota *corev1.ResourceQuota) error { + // skipping namespaced resource quota + if newQuota.APIVersion != quotav1alpha2.SchemeGroupVersion.String() { + klog.V(6).Infof("skipping namespaced resource quota %v %v", newQuota.Namespace, newQuota.Name) + return nil + } + ctx := context.TODO() + resourceQuota := "av1alpha2.ResourceQuota{} + err := a.client.Get(ctx, types.NamespacedName{Name: newQuota.Name}, resourceQuota) + if err != nil { + klog.Errorf("failed to fetch resource quota: %s, %v", newQuota.Name, err) + return err + } + resourceQuota = a.checkCache(resourceQuota) + + // re-assign objectmeta + // make a copy + updatedQuota := resourceQuota.DeepCopy() + updatedQuota.ObjectMeta = newQuota.ObjectMeta + updatedQuota.Namespace = "" + + // determine change in usage + usageDiff := utilquota.Subtract(newQuota.Status.Used, updatedQuota.Status.Total.Used) + + // update aggregate usage + updatedQuota.Status.Total.Used = newQuota.Status.Used + + // update per namespace totals + oldNamespaceTotals, _ := getResourceQuotasStatusByNamespace(updatedQuota.Status.Namespaces, newQuota.Namespace) + namespaceTotalCopy := oldNamespaceTotals.DeepCopy() + newNamespaceTotals := *namespaceTotalCopy + newNamespaceTotals.Used = utilquota.Add(oldNamespaceTotals.Used, usageDiff) + insertResourceQuotasStatus(&updatedQuota.Status.Namespaces, quotav1alpha2.ResourceQuotaStatusByNamespace{ + Namespace: newQuota.Namespace, + ResourceQuotaStatus: newNamespaceTotals, + }) + + klog.V(6).Infof("update resource quota: %+v", updatedQuota) + err = a.client.Status().Update(ctx, updatedQuota, &client.UpdateOptions{}) + if err != nil { + klog.Errorf("failed to update resource quota: %v", err) + return err + } + + a.updatedResourceQuotas.Add(resourceQuota.Name, updatedQuota) + return nil +} + +var etcdVersioner = etcd.APIObjectVersioner{} + +// checkCache compares the passed quota against the value in the look-aside cache and returns the newer +// if the cache is out of date, it deletes the stale entry. This only works because of etcd resourceVersions +// being monotonically increasing integers +func (a *accessor) checkCache(resourceQuota *quotav1alpha2.ResourceQuota) *quotav1alpha2.ResourceQuota { + uncastCachedQuota, ok := a.updatedResourceQuotas.Get(resourceQuota.Name) + if !ok { + return resourceQuota + } + cachedQuota := uncastCachedQuota.(*quotav1alpha2.ResourceQuota) + + if etcdVersioner.CompareResourceVersion(resourceQuota, cachedQuota) >= 0 { + a.updatedResourceQuotas.Remove(resourceQuota.Name) + return resourceQuota + } + return cachedQuota +} + +func (a *accessor) GetQuotas(namespaceName string) ([]corev1.ResourceQuota, error) { + resourceQuotaNames, err := a.waitForReadyResourceQuotaNames(namespaceName) + if err != nil { + klog.Errorf("failed to fetch resource quota names: %v, %v", namespaceName, err) + return nil, err + } + var result []corev1.ResourceQuota + for _, resourceQuotaName := range resourceQuotaNames { + resourceQuota := "av1alpha2.ResourceQuota{} + err = a.client.Get(context.TODO(), types.NamespacedName{Name: resourceQuotaName}, resourceQuota) + if err != nil { + klog.Errorf("failed to fetch resource quota %s: %v", resourceQuotaName, err) + return result, err + } + resourceQuota = a.checkCache(resourceQuota) + + // now convert to a ResourceQuota + convertedQuota := corev1.ResourceQuota{} + convertedQuota.APIVersion = quotav1alpha2.SchemeGroupVersion.String() + convertedQuota.ObjectMeta = resourceQuota.ObjectMeta + convertedQuota.Namespace = namespaceName + convertedQuota.Spec = resourceQuota.Spec.Quota + convertedQuota.Status = resourceQuota.Status.Total + result = append(result, convertedQuota) + } + + // avoid conflicts with namespaced resource quota + namespacedResourceQuotas, err := a.waitForReadyNamespacedResourceQuotas(namespaceName) + if err != nil { + klog.Errorf("failed to fetch namespaced resource quotas: %v, %v", namespaceName, err) + return nil, err + } + for _, resourceQuota := range namespacedResourceQuotas { + resourceQuota.APIVersion = corev1.SchemeGroupVersion.String() + result = append(result, resourceQuota) + } + return result, nil +} + +func (a *accessor) waitForReadyResourceQuotaNames(namespaceName string) ([]string, error) { + ctx := context.TODO() + var resourceQuotaNames []string + var err error + // wait for a valid mapping cache. The overall response can be delayed for up to 10 seconds. + err = utilwait.PollImmediate(100*time.Millisecond, 8*time.Second, func() (done bool, err error) { + resourceQuotaNames, err = resourceQuotaNamesFor(ctx, a.client, namespaceName) + // if we can't find the namespace yet, just wait for the cache to update. Requests to non-existent namespaces + // may hang, but those people are doing something wrong and namespace lifecycle should reject them. + if apierrors.IsNotFound(err) { + return false, nil + } + if err != nil { + return false, err + } + return true, nil + }) + return resourceQuotaNames, err +} + +func (a *accessor) waitForReadyNamespacedResourceQuotas(namespaceName string) ([]corev1.ResourceQuota, error) { + ctx := context.TODO() + var resourceQuotas []corev1.ResourceQuota + var err error + // wait for a valid mapping cache. The overall response can be delayed for up to 10 seconds. + err = utilwait.PollImmediate(100*time.Millisecond, 8*time.Second, func() (done bool, err error) { + resourceQuotaList := &corev1.ResourceQuotaList{} + err = a.client.List(ctx, resourceQuotaList, &client.ListOptions{Namespace: namespaceName}) + if err != nil { + return false, err + } + resourceQuotas = resourceQuotaList.Items + return true, nil + }) + return resourceQuotas, err +} diff --git a/pkg/controller/quota/lockfactory.go b/pkg/controller/quota/lockfactory.go new file mode 100644 index 000000000..7c5af60e6 --- /dev/null +++ b/pkg/controller/quota/lockfactory.go @@ -0,0 +1,59 @@ +/* + + Copyright 2021 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. + +*/ + +package quota + +import ( + "sync" +) + +// Following code copied from github.com/openshift/apiserver-library-go/pkg/admission/quota/clusterresourcequota +type LockFactory interface { + GetLock(string) sync.Locker +} + +type DefaultLockFactory struct { + lock sync.RWMutex + + locks map[string]sync.Locker +} + +func NewDefaultLockFactory() *DefaultLockFactory { + return &DefaultLockFactory{locks: map[string]sync.Locker{}} +} + +func (f *DefaultLockFactory) GetLock(key string) sync.Locker { + lock, exists := f.getExistingLock(key) + if exists { + return lock + } + + f.lock.Lock() + defer f.lock.Unlock() + lock = &sync.Mutex{} + f.locks[key] = lock + return lock +} + +func (f *DefaultLockFactory) getExistingLock(key string) (sync.Locker, bool) { + f.lock.RLock() + defer f.lock.RUnlock() + + lock, exists := f.locks[key] + return lock, exists +} diff --git a/pkg/controller/quota/resourcequota_controller.go b/pkg/controller/quota/resourcequota_controller.go new file mode 100644 index 000000000..5593e07a4 --- /dev/null +++ b/pkg/controller/quota/resourcequota_controller.go @@ -0,0 +1,299 @@ +/* + + Copyright 2021 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. + +*/ + +package quota + +import ( + "context" + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/clock" + "k8s.io/client-go/tools/record" + "k8s.io/klog" + evaluatorcore "kubesphere.io/kubesphere/kube/pkg/quota/v1/evaluator/core" + "kubesphere.io/kubesphere/kube/pkg/quota/v1/generic" + "kubesphere.io/kubesphere/kube/pkg/quota/v1/install" + quotav1alpha2 "kubesphere.io/kubesphere/pkg/apis/quota/v1alpha2" + tenantv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1" + "kubesphere.io/kubesphere/pkg/constants" + "kubesphere.io/kubesphere/pkg/utils/sliceutil" + "math" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + "time" + + k8sinformers "k8s.io/client-go/informers" + "sigs.k8s.io/controller-runtime/pkg/client" + + corev1 "k8s.io/api/core/v1" + + quotav1 "kubesphere.io/kubesphere/kube/pkg/quota/v1" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/controller" +) + +const ( + ControllerName = "resourcequota-controller" + DefaultResyncPeriod = 5 * time.Minute + DefaultMaxConcurrentReconciles = 8 +) + +// Reconciler reconciles a Workspace object +type Reconciler struct { + client.Client + logger logr.Logger + recorder record.EventRecorder + maxConcurrentReconciles int + // Knows how to calculate usage + registry quotav1.Registry + // Controls full recalculation of quota usage + resyncPeriod time.Duration + scheme *runtime.Scheme +} + +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, maxConcurrentReconciles int, resyncPeriod time.Duration, informerFactory k8sinformers.SharedInformerFactory) error { + r.logger = ctrl.Log.WithName("controllers").WithName(ControllerName) + r.recorder = mgr.GetEventRecorderFor(ControllerName) + r.scheme = mgr.GetScheme() + r.registry = generic.NewRegistry(install.NewQuotaConfigurationForControllers(generic.ListerFuncForResourceFunc(informerFactory.ForResource)).Evaluators()) + if r.Client == nil { + r.Client = mgr.GetClient() + } + if maxConcurrentReconciles > 0 { + r.maxConcurrentReconciles = maxConcurrentReconciles + } else { + r.maxConcurrentReconciles = DefaultMaxConcurrentReconciles + } + r.resyncPeriod = time.Duration(math.Max(float64(resyncPeriod), float64(DefaultResyncPeriod))) + c, err := ctrl.NewControllerManagedBy(mgr). + Named(ControllerName). + WithOptions(controller.Options{ + MaxConcurrentReconciles: r.maxConcurrentReconciles, + }). + For("av1alpha2.ResourceQuota{}). + WithEventFilter(predicate.GenerationChangedPredicate{ + Funcs: predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { + oldQuota := e.ObjectOld.(*quotav1alpha2.ResourceQuota) + newQuota := e.ObjectNew.(*quotav1alpha2.ResourceQuota) + return !equality.Semantic.DeepEqual(oldQuota.Spec, newQuota.Spec) + }, + }, + }). + Build(r) + if err != nil { + return err + } + + resources := []runtime.Object{ + &corev1.Pod{}, + &corev1.Service{}, + &corev1.PersistentVolumeClaim{}, + } + realClock := clock.RealClock{} + for _, resource := range resources { + err := c.Watch( + &source.Kind{Type: resource}, + &handler.EnqueueRequestsFromMapFunc{ToRequests: handler.ToRequestsFunc(r.mapper)}, + predicate.Funcs{ + GenericFunc: func(e event.GenericEvent) bool { + return false + }, + CreateFunc: func(e event.CreateEvent) bool { + return false + }, + UpdateFunc: func(e event.UpdateEvent) bool { + notifyChange := false + // we only want to queue the updates we care about though as too much noise will overwhelm queue. + switch e.MetaOld.(type) { + case *corev1.Pod: + oldPod := e.ObjectOld.(*corev1.Pod) + newPod := e.ObjectNew.(*corev1.Pod) + notifyChange = evaluatorcore.QuotaV1Pod(oldPod, realClock) && !evaluatorcore.QuotaV1Pod(newPod, realClock) + case *corev1.Service: + oldService := e.ObjectOld.(*corev1.Service) + newService := e.ObjectNew.(*corev1.Service) + notifyChange = evaluatorcore.GetQuotaServiceType(oldService) != evaluatorcore.GetQuotaServiceType(newService) + case *corev1.PersistentVolumeClaim: + notifyChange = true + } + return notifyChange + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return true + }, + }) + if err != nil { + return err + } + } + return nil +} + +func (r *Reconciler) mapper(h handler.MapObject) []reconcile.Request { + // check if the quota controller can evaluate this kind, if not, ignore it altogether... + var result []reconcile.Request + evaluators := r.registry.List() + ctx := context.TODO() + resourceQuotaNames, err := resourceQuotaNamesFor(ctx, r.Client, h.Meta.GetNamespace()) + if err != nil { + klog.Errorf("failed to get resource quota names for: %v %T %v, err: %v", h.Meta.GetNamespace(), h.Object, h.Meta.GetName(), err) + return result + } + // only queue those quotas that are tracking a resource associated with this kind. + for _, resourceQuotaName := range resourceQuotaNames { + resourceQuota := "av1alpha2.ResourceQuota{} + if err := r.Get(ctx, types.NamespacedName{Name: resourceQuotaName}, resourceQuota); err != nil { + klog.Errorf("failed to get resource quota: %v, err: %v", resourceQuotaName, err) + return result + } + resourceQuotaResources := quotav1.ResourceNames(resourceQuota.Status.Total.Hard) + for _, evaluator := range evaluators { + matchedResources := evaluator.MatchingResources(resourceQuotaResources) + if len(matchedResources) > 0 { + result = append(result, reconcile.Request{NamespacedName: types.NamespacedName{Name: resourceQuotaName}}) + break + } + } + } + klog.V(6).Infof("resource quota reconcile after resource change: %v %T %v, %+v", h.Meta.GetNamespace(), h.Object, h.Meta.GetName(), result) + return result +} + +func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + logger := r.logger.WithValues("resourcequota", req.NamespacedName) + rootCtx := context.TODO() + resourceQuota := "av1alpha2.ResourceQuota{} + if err := r.Get(rootCtx, req.NamespacedName, resourceQuota); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + if err := r.bindWorkspace(resourceQuota); err != nil { + logger.Error(err, "failed to set owner reference") + return ctrl.Result{}, err + } + + if err := r.syncQuotaForNamespaces(resourceQuota); err != nil { + logger.Error(err, "failed to sync quota") + return ctrl.Result{}, err + } + + r.recorder.Event(resourceQuota, corev1.EventTypeNormal, "Synced", "Synced successfully") + return ctrl.Result{RequeueAfter: r.resyncPeriod}, nil +} + +func (r *Reconciler) bindWorkspace(resourceQuota *quotav1alpha2.ResourceQuota) error { + workspaceName := resourceQuota.Labels[constants.WorkspaceLabelKey] + if workspaceName == "" { + return nil + } + + workspace := &tenantv1alpha1.Workspace{} + err := r.Get(context.TODO(), types.NamespacedName{Name: workspaceName}, workspace) + if err != nil { + return client.IgnoreNotFound(err) + } + + if !metav1.IsControlledBy(resourceQuota, workspace) { + resourceQuota.OwnerReferences = nil + if err := controllerutil.SetControllerReference(workspace, resourceQuota, r.scheme); err != nil { + return err + } + err = r.Update(context.TODO(), resourceQuota) + if err != nil { + klog.Error(err) + return err + } + } + + return nil +} + +func (r *Reconciler) syncQuotaForNamespaces(originalQuota *quotav1alpha2.ResourceQuota) error { + quota := originalQuota.DeepCopy() + ctx := context.TODO() + // get the list of namespaces that match this cluster quota + matchingNamespaceList := corev1.NamespaceList{} + if err := r.List(ctx, &matchingNamespaceList, &client.ListOptions{LabelSelector: labels.SelectorFromSet(quota.Spec.LabelSelector)}); err != nil { + return err + } + + matchingNamespaceNames := make([]string, 0) + for _, namespace := range matchingNamespaceList.Items { + matchingNamespaceNames = append(matchingNamespaceNames, namespace.Name) + } + + for _, namespace := range matchingNamespaceList.Items { + namespaceName := namespace.Name + namespaceTotals, _ := getResourceQuotasStatusByNamespace(quota.Status.Namespaces, namespaceName) + + actualUsage, err := quotaUsageCalculationFunc(namespaceName, quota.Spec.Quota.Scopes, quota.Spec.Quota.Hard, r.registry, quota.Spec.Quota.ScopeSelector) + if err != nil { + return err + } + recalculatedStatus := corev1.ResourceQuotaStatus{ + Used: actualUsage, + Hard: quota.Spec.Quota.Hard, + } + + // subtract old usage, add new usage + quota.Status.Total.Used = quotav1.Subtract(quota.Status.Total.Used, namespaceTotals.Used) + quota.Status.Total.Used = quotav1.Add(quota.Status.Total.Used, recalculatedStatus.Used) + insertResourceQuotasStatus("a.Status.Namespaces, quotav1alpha2.ResourceQuotaStatusByNamespace{ + Namespace: namespaceName, + ResourceQuotaStatus: recalculatedStatus, + }) + } + + // Remove any namespaces from quota.status that no longer match. + statusCopy := quota.Status.Namespaces.DeepCopy() + for _, namespaceTotals := range statusCopy { + namespaceName := namespaceTotals.Namespace + if !sliceutil.HasString(matchingNamespaceNames, namespaceName) { + quota.Status.Total.Used = quotav1.Subtract(quota.Status.Total.Used, namespaceTotals.Used) + removeResourceQuotasStatusByNamespace("a.Status.Namespaces, namespaceName) + } + } + + quota.Status.Total.Hard = quota.Spec.Quota.Hard + + // if there's no change, no update, return early. NewAggregate returns nil on empty input + if equality.Semantic.DeepEqual(quota, originalQuota) { + return nil + } + + klog.V(6).Infof("update resource quota: %+v", quota) + if err := r.Status().Update(ctx, quota, &client.UpdateOptions{}); err != nil { + return err + } + + return nil +} + +// quotaUsageCalculationFunc is a function to calculate quota usage. It is only configurable for easy unit testing +// NEVER CHANGE THIS OUTSIDE A TEST +var quotaUsageCalculationFunc = quotav1.CalculateUsage diff --git a/pkg/controller/quota/resourcequota_webhook.go b/pkg/controller/quota/resourcequota_webhook.go new file mode 100644 index 000000000..5f047ebb3 --- /dev/null +++ b/pkg/controller/quota/resourcequota_webhook.go @@ -0,0 +1,191 @@ +/* + + Copyright 2021 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. + +*/ + +package quota + +import ( + "context" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + utilwait "k8s.io/apimachinery/pkg/util/wait" + admissionapi "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/klog" + "kubesphere.io/kubesphere/kube/pkg/quota/v1" + "kubesphere.io/kubesphere/kube/pkg/quota/v1/generic" + "kubesphere.io/kubesphere/kube/pkg/quota/v1/install" + "kubesphere.io/kubesphere/kube/plugin/pkg/admission/resourcequota" + resourcequotaapi "kubesphere.io/kubesphere/kube/plugin/pkg/admission/resourcequota/apis/resourcequota" + "net/http" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + "sort" + "sync" +) + +const ( + numEvaluatorThreads = 10 +) + +type ResourceQuotaAdmission struct { + client client.Client + + decoder *webhook.AdmissionDecoder + + lockFactory LockFactory + + // these are used to create the evaluator + registry quota.Registry + + init sync.Once + evaluator resourcequota.Evaluator +} + +func NewResourceQuotaAdmission(client client.Client, scheme *runtime.Scheme) (webhook.AdmissionHandler, error) { + decoder, err := admission.NewDecoder(scheme) + if err != nil { + return nil, err + } + return &ResourceQuotaAdmission{ + client: client, + lockFactory: NewDefaultLockFactory(), + decoder: decoder, + registry: generic.NewRegistry(install.NewQuotaConfigurationForAdmission().Evaluators()), + }, nil +} + +func (r *ResourceQuotaAdmission) Handle(ctx context.Context, req webhook.AdmissionRequest) webhook.AdmissionResponse { + // ignore all operations that correspond to sub-resource actions + if len(req.RequestSubResource) != 0 { + return webhook.Allowed("") + } + // ignore cluster level resources + if len(req.Namespace) == 0 { + return webhook.Allowed("") + } + + r.init.Do(func() { + resourceQuotaAccessor := newQuotaAccessor(r.client) + r.evaluator = resourcequota.NewQuotaEvaluator(resourceQuotaAccessor, install.DefaultIgnoredResources(), r.registry, r.lockAquisition, &resourcequotaapi.Configuration{}, numEvaluatorThreads, utilwait.NeverStop) + }) + + attributesRecord, err := convertToAdmissionAttributes(req) + if err != nil { + klog.Error(err) + return webhook.Errored(http.StatusBadRequest, err) + } + + if err := r.evaluator.Evaluate(attributesRecord); err != nil { + if errors.IsForbidden(err) { + klog.Info(err) + return webhook.Denied(err.Error()) + } + klog.Error(err) + return webhook.Errored(http.StatusInternalServerError, err) + } + + return webhook.Allowed("") +} + +type ByName []corev1.ResourceQuota + +func (v ByName) Len() int { return len(v) } +func (v ByName) Swap(i, j int) { v[i], v[j] = v[j], v[i] } +func (v ByName) Less(i, j int) bool { return v[i].Name < v[j].Name } + +func (r *ResourceQuotaAdmission) lockAquisition(quotas []corev1.ResourceQuota) func() { + var locks []sync.Locker + + // acquire the locks in alphabetical order because I'm too lazy to think of something clever + sort.Sort(ByName(quotas)) + for _, quota := range quotas { + lock := r.lockFactory.GetLock(string(quota.UID)) + lock.Lock() + locks = append(locks, lock) + } + + return func() { + for i := len(locks) - 1; i >= 0; i-- { + locks[i].Unlock() + } + } +} + +func convertToAdmissionAttributes(req admission.Request) (admissionapi.Attributes, error) { + var err error + var object runtime.Object + if len(req.Object.Raw) > 0 { + object, _, err = scheme.Codecs.UniversalDeserializer().Decode(req.Object.Raw, nil, nil) + if err != nil { + return nil, err + } + } + + var oldObject runtime.Object + if len(req.OldObject.Raw) > 0 { + oldObject, _, err = scheme.Codecs.UniversalDeserializer().Decode(req.OldObject.Raw, nil, nil) + if err != nil { + klog.Error(err) + return nil, err + } + } + + var operationOptions runtime.Object + if len(req.Options.Raw) > 0 { + operationOptions, _, err = scheme.Codecs.UniversalDeserializer().Decode(req.Options.Raw, nil, nil) + if err != nil { + klog.Error(err) + return nil, err + } + } + + extras := map[string][]string{} + for k, v := range req.UserInfo.Extra { + extras[k] = v + } + + attributesRecord := admissionapi.NewAttributesRecord(object, + oldObject, + schema.GroupVersionKind{ + Group: req.RequestKind.Group, + Version: req.RequestKind.Version, + Kind: req.RequestKind.Kind, + }, + req.Namespace, + req.Name, + schema.GroupVersionResource{ + Group: req.RequestResource.Group, + Version: req.RequestResource.Version, + Resource: req.RequestResource.Resource, + }, + req.SubResource, + admissionapi.Operation(req.Operation), + operationOptions, + *req.DryRun, + &user.DefaultInfo{ + Name: req.UserInfo.Username, + UID: req.UserInfo.UID, + Groups: req.UserInfo.Groups, + Extra: extras, + }) + return attributesRecord, nil +} diff --git a/pkg/controller/quota/util.go b/pkg/controller/quota/util.go new file mode 100644 index 000000000..0a8557503 --- /dev/null +++ b/pkg/controller/quota/util.go @@ -0,0 +1,92 @@ +/* + + Copyright 2021 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. + +*/ + +package quota + +import ( + "context" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + quotav1alpha2 "kubesphere.io/kubesphere/pkg/apis/quota/v1alpha2" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Following code copied from github.com/openshift/library-go/pkg/quota/quotautil +func getResourceQuotasStatusByNamespace(namespaceStatuses quotav1alpha2.ResourceQuotasStatusByNamespace, namespace string) (corev1.ResourceQuotaStatus, bool) { + for i := range namespaceStatuses { + curr := namespaceStatuses[i] + if curr.Namespace == namespace { + return curr.ResourceQuotaStatus, true + } + } + return corev1.ResourceQuotaStatus{}, false +} + +func removeResourceQuotasStatusByNamespace(namespaceStatuses *quotav1alpha2.ResourceQuotasStatusByNamespace, namespace string) { + newNamespaceStatuses := quotav1alpha2.ResourceQuotasStatusByNamespace{} + for i := range *namespaceStatuses { + curr := (*namespaceStatuses)[i] + if curr.Namespace == namespace { + continue + } + newNamespaceStatuses = append(newNamespaceStatuses, curr) + } + *namespaceStatuses = newNamespaceStatuses +} + +func insertResourceQuotasStatus(namespaceStatuses *quotav1alpha2.ResourceQuotasStatusByNamespace, newStatus quotav1alpha2.ResourceQuotaStatusByNamespace) { + newNamespaceStatuses := quotav1alpha2.ResourceQuotasStatusByNamespace{} + found := false + for i := range *namespaceStatuses { + curr := (*namespaceStatuses)[i] + if curr.Namespace == newStatus.Namespace { + // do this so that we don't change serialization order + newNamespaceStatuses = append(newNamespaceStatuses, newStatus) + found = true + continue + } + newNamespaceStatuses = append(newNamespaceStatuses, curr) + } + if !found { + newNamespaceStatuses = append(newNamespaceStatuses, newStatus) + } + *namespaceStatuses = newNamespaceStatuses +} + +func resourceQuotaNamesFor(ctx context.Context, client client.Client, namespaceName string) ([]string, error) { + namespace := &corev1.Namespace{} + var resourceQuotaNames []string + if err := client.Get(ctx, types.NamespacedName{Name: namespaceName}, namespace); err != nil { + return resourceQuotaNames, err + } + if len(namespace.Labels) == 0 { + return resourceQuotaNames, nil + } + resourceQuotaList := "av1alpha2.ResourceQuotaList{} + if err := client.List(ctx, resourceQuotaList); err != nil { + return resourceQuotaNames, err + } + for _, resourceQuota := range resourceQuotaList.Items { + if len(resourceQuota.Spec.LabelSelector) > 0 && + labels.SelectorFromSet(resourceQuota.Spec.LabelSelector).Matches(labels.Set(namespace.Labels)) { + resourceQuotaNames = append(resourceQuotaNames, resourceQuota.Name) + } + } + return resourceQuotaNames, nil +} diff --git a/pkg/kapis/tenant/v1alpha2/handler.go b/pkg/kapis/tenant/v1alpha2/handler.go index 65bddea45..617b4678c 100644 --- a/pkg/kapis/tenant/v1alpha2/handler.go +++ b/pkg/kapis/tenant/v1alpha2/handler.go @@ -29,6 +29,7 @@ import ( auditingv1alpha1 "kubesphere.io/kubesphere/pkg/api/auditing/v1alpha1" eventsv1alpha1 "kubesphere.io/kubesphere/pkg/api/events/v1alpha1" loggingv1alpha2 "kubesphere.io/kubesphere/pkg/api/logging/v1alpha2" + quotav1alpha2 "kubesphere.io/kubesphere/pkg/apis/quota/v1alpha2" tenantv1alpha2 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha2" "kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer" "kubesphere.io/kubesphere/pkg/apiserver/query" @@ -546,3 +547,78 @@ func (h *tenantHandler) ListClusters(r *restful.Request, response *restful.Respo response.WriteEntity(result) } + +func (h *tenantHandler) CreateWorkspaceResourceQuota(r *restful.Request, response *restful.Response) { + workspaceName := r.PathParameter("workspace") + resourceQuota := "av1alpha2.ResourceQuota{} + err := r.ReadEntity(resourceQuota) + if err != nil { + api.HandleBadRequest(response, r, err) + return + } + result, err := h.tenant.CreateWorkspaceResourceQuota(workspaceName, resourceQuota) + if err != nil { + api.HandleInternalError(response, r, err) + return + } + response.WriteEntity(result) +} + +func (h *tenantHandler) DeleteWorkspaceResourceQuota(r *restful.Request, response *restful.Response) { + workspace := r.PathParameter("workspace") + resourceQuota := r.PathParameter("resourcequota") + + if err := h.tenant.DeleteWorkspaceResourceQuota(workspace, resourceQuota); err != nil { + if errors.IsNotFound(err) { + api.HandleNotFound(response, r, err) + return + } + api.HandleInternalError(response, r, err) + return + } + + response.WriteEntity(servererr.None) +} + +func (h *tenantHandler) UpdateWorkspaceResourceQuota(r *restful.Request, response *restful.Response) { + workspaceName := r.PathParameter("workspace") + resourceQuotaName := r.PathParameter("resourcequota") + resourceQuota := "av1alpha2.ResourceQuota{} + err := r.ReadEntity(resourceQuota) + if err != nil { + api.HandleBadRequest(response, r, err) + return + } + + if resourceQuotaName != resourceQuota.Name { + err := fmt.Errorf("the name of the object (%s) does not match the name on the URL (%s)", resourceQuota.Name, resourceQuotaName) + klog.Errorf("%+v", err) + api.HandleBadRequest(response, r, err) + return + } + + result, err := h.tenant.UpdateWorkspaceResourceQuota(workspaceName, resourceQuota) + if err != nil { + api.HandleInternalError(response, r, err) + return + } + + response.WriteEntity(result) +} + +func (h *tenantHandler) DescribeWorkspaceResourceQuota(r *restful.Request, response *restful.Response) { + workspaceName := r.PathParameter("workspace") + resourceQuotaName := r.PathParameter("resourcequota") + + resourceQuota, err := h.tenant.DescribeWorkspaceResourceQuota(workspaceName, resourceQuotaName) + if err != nil { + if errors.IsNotFound(err) { + api.HandleNotFound(response, r, err) + return + } + api.HandleInternalError(response, r, err) + return + } + + response.WriteEntity(resourceQuota) +} diff --git a/pkg/kapis/tenant/v1alpha2/register.go b/pkg/kapis/tenant/v1alpha2/register.go index cf295e5e4..868b6244b 100644 --- a/pkg/kapis/tenant/v1alpha2/register.go +++ b/pkg/kapis/tenant/v1alpha2/register.go @@ -26,6 +26,7 @@ import ( auditingv1alpha1 "kubesphere.io/kubesphere/pkg/api/auditing/v1alpha1" eventsv1alpha1 "kubesphere.io/kubesphere/pkg/api/events/v1alpha1" loggingv1alpha2 "kubesphere.io/kubesphere/pkg/api/logging/v1alpha2" + quotav1alpha2 "kubesphere.io/kubesphere/pkg/apis/quota/v1alpha2" tenantv1alpha2 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha2" "kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer" "kubesphere.io/kubesphere/pkg/apiserver/runtime" @@ -288,6 +289,38 @@ func AddToContainer(c *restful.Container, factory informers.InformerFactory, k8s Writes(auditingv1alpha1.APIResponse{}). Returns(http.StatusOK, api.StatusOK, auditingv1alpha1.APIResponse{})) + ws.Route(ws.POST("/workspaces/{workspace}/resourcequotas"). + To(handler.CreateWorkspaceResourceQuota). + Reads(quotav1alpha2.ResourceQuota{}). + Returns(http.StatusOK, api.StatusOK, quotav1alpha2.ResourceQuota{}). + Doc("Create resource quota."). + Metadata(restfulspec.KeyOpenAPITags, []string{constants.WorkspaceTag})) + + ws.Route(ws.DELETE("/workspaces/{workspace}/resourcequotas/{resourcequota}"). + To(handler.DeleteWorkspaceResourceQuota). + Param(ws.PathParameter("workspace", "workspace name")). + Param(ws.PathParameter("resourcequota", "resource quota name")). + Returns(http.StatusOK, api.StatusOK, errors.None). + Doc("Delete resource quota."). + Metadata(restfulspec.KeyOpenAPITags, []string{constants.WorkspaceTag})) + + ws.Route(ws.PUT("/workspaces/{workspace}/resourcequotas/{resourcequota}"). + To(handler.UpdateWorkspaceResourceQuota). + Param(ws.PathParameter("workspace", "workspace name")). + Param(ws.PathParameter("resourcequota", "resource quota name")). + Reads(quotav1alpha2.ResourceQuota{}). + Returns(http.StatusOK, api.StatusOK, quotav1alpha2.ResourceQuota{}). + Doc("Update resource quota."). + Metadata(restfulspec.KeyOpenAPITags, []string{constants.WorkspaceTag})) + + ws.Route(ws.GET("/workspaces/{workspace}/resourcequotas/{resourcequota}"). + To(handler.DescribeWorkspaceResourceQuota). + Param(ws.PathParameter("workspace", "workspace name")). + Param(ws.PathParameter("resourcequota", "resource quota name")). + Returns(http.StatusOK, api.StatusOK, quotav1alpha2.ResourceQuota{}). + Doc("Describe resource quota."). + Metadata(restfulspec.KeyOpenAPITags, []string{constants.WorkspaceTag})) + c.Add(ws) return nil } diff --git a/pkg/models/tenant/resourcequota.go b/pkg/models/tenant/resourcequota.go new file mode 100644 index 000000000..22272ed95 --- /dev/null +++ b/pkg/models/tenant/resourcequota.go @@ -0,0 +1,75 @@ +/* + + Copyright 2021 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. + +*/ + +package tenant + +import ( + "context" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + quotav1alpha2 "kubesphere.io/kubesphere/pkg/apis/quota/v1alpha2" + tenantv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1" +) + +func (t *tenantOperator) CreateWorkspaceResourceQuota(workspace string, quota *quotav1alpha2.ResourceQuota) (*quotav1alpha2.ResourceQuota, error) { + if quota.Labels == nil { + quota.Labels = make(map[string]string) + } + quota.Labels[tenantv1alpha1.WorkspaceLabel] = workspace + quota.Spec.LabelSelector = labels.Set{tenantv1alpha1.WorkspaceLabel: workspace} + return t.ksclient.QuotaV1alpha2().ResourceQuotas().Create(context.TODO(), quota, metav1.CreateOptions{}) +} + +func (t *tenantOperator) UpdateWorkspaceResourceQuota(workspace string, quota *quotav1alpha2.ResourceQuota) (*quotav1alpha2.ResourceQuota, error) { + resourceQuota, err := t.ksclient.QuotaV1alpha2().ResourceQuotas().Get(context.TODO(), quota.Name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + if resourceQuota.Labels[tenantv1alpha1.WorkspaceLabel] != workspace { + return nil, errors.NewNotFound(quotav1alpha2.Resource(quotav1alpha2.ResourcesSingularCluster), resourceQuota.Name) + } + if quota.Labels == nil { + quota.Labels = make(map[string]string) + } + quota.Labels[tenantv1alpha1.WorkspaceLabel] = workspace + quota.Spec.LabelSelector = labels.Set{tenantv1alpha1.WorkspaceLabel: workspace} + return t.ksclient.QuotaV1alpha2().ResourceQuotas().Update(context.TODO(), quota, metav1.UpdateOptions{}) +} + +func (t *tenantOperator) DeleteWorkspaceResourceQuota(workspace string, resourceQuotaName string) error { + resourceQuota, err := t.ksclient.QuotaV1alpha2().ResourceQuotas().Get(context.TODO(), resourceQuotaName, metav1.GetOptions{}) + if err != nil { + return err + } + if resourceQuota.Labels[tenantv1alpha1.WorkspaceLabel] != workspace { + return errors.NewNotFound(quotav1alpha2.Resource(quotav1alpha2.ResourcesSingularCluster), resourceQuotaName) + } + return t.ksclient.QuotaV1alpha2().ResourceQuotas().Delete(context.TODO(), resourceQuotaName, metav1.DeleteOptions{}) +} + +func (t *tenantOperator) DescribeWorkspaceResourceQuota(workspace string, resourceQuotaName string) (*quotav1alpha2.ResourceQuota, error) { + resourceQuota, err := t.ksclient.QuotaV1alpha2().ResourceQuotas().Get(context.TODO(), resourceQuotaName, metav1.GetOptions{}) + if err != nil { + return nil, err + } + if resourceQuota.Labels[tenantv1alpha1.WorkspaceLabel] != workspace { + return nil, errors.NewNotFound(quotav1alpha2.Resource(quotav1alpha2.ResourcesSingularCluster), resourceQuotaName) + } + return resourceQuota, nil +} diff --git a/pkg/models/tenant/tenant.go b/pkg/models/tenant/tenant.go index 86e1ffd76..8d0cd8a47 100644 --- a/pkg/models/tenant/tenant.go +++ b/pkg/models/tenant/tenant.go @@ -38,6 +38,7 @@ import ( eventsv1alpha1 "kubesphere.io/kubesphere/pkg/api/events/v1alpha1" loggingv1alpha2 "kubesphere.io/kubesphere/pkg/api/logging/v1alpha2" clusterv1alpha1 "kubesphere.io/kubesphere/pkg/apis/cluster/v1alpha1" + quotav1alpha2 "kubesphere.io/kubesphere/pkg/apis/quota/v1alpha2" tenantv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1" tenantv1alpha2 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha2" typesv1beta1 "kubesphere.io/kubesphere/pkg/apis/types/v1beta1" @@ -79,6 +80,10 @@ type Interface interface { PatchNamespace(workspace string, namespace *corev1.Namespace) (*corev1.Namespace, error) PatchWorkspace(workspace string, data json.RawMessage) (*tenantv1alpha2.WorkspaceTemplate, error) ListClusters(info user.Info) (*api.ListResult, error) + CreateWorkspaceResourceQuota(workspace string, resourceQuota *quotav1alpha2.ResourceQuota) (*quotav1alpha2.ResourceQuota, error) + DeleteWorkspaceResourceQuota(workspace string, resourceQuotaName string) error + UpdateWorkspaceResourceQuota(workspace string, resourceQuota *quotav1alpha2.ResourceQuota) (*quotav1alpha2.ResourceQuota, error) + DescribeWorkspaceResourceQuota(workspace string, resourceQuotaName string) (*quotav1alpha2.ResourceQuota, error) } type tenantOperator struct { diff --git a/vendor/k8s.io/client-go/metadata/metadatainformer/informer.go b/vendor/k8s.io/client-go/metadata/metadatainformer/informer.go new file mode 100644 index 000000000..e4ebd61f8 --- /dev/null +++ b/vendor/k8s.io/client-go/metadata/metadatainformer/informer.go @@ -0,0 +1,157 @@ +/* +Copyright 2018 The Kubernetes 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. +*/ + +package metadatainformer + +import ( + "context" + "sync" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/informers" + "k8s.io/client-go/metadata" + "k8s.io/client-go/metadata/metadatalister" + "k8s.io/client-go/tools/cache" +) + +// NewSharedInformerFactory constructs a new instance of metadataSharedInformerFactory for all namespaces. +func NewSharedInformerFactory(client metadata.Interface, defaultResync time.Duration) SharedInformerFactory { + return NewFilteredSharedInformerFactory(client, defaultResync, metav1.NamespaceAll, nil) +} + +// NewFilteredSharedInformerFactory constructs a new instance of metadataSharedInformerFactory. +// Listers obtained via this factory will be subject to the same filters as specified here. +func NewFilteredSharedInformerFactory(client metadata.Interface, defaultResync time.Duration, namespace string, tweakListOptions TweakListOptionsFunc) SharedInformerFactory { + return &metadataSharedInformerFactory{ + client: client, + defaultResync: defaultResync, + namespace: namespace, + informers: map[schema.GroupVersionResource]informers.GenericInformer{}, + startedInformers: make(map[schema.GroupVersionResource]bool), + tweakListOptions: tweakListOptions, + } +} + +type metadataSharedInformerFactory struct { + client metadata.Interface + defaultResync time.Duration + namespace string + + lock sync.Mutex + informers map[schema.GroupVersionResource]informers.GenericInformer + // startedInformers is used for tracking which informers have been started. + // This allows Start() to be called multiple times safely. + startedInformers map[schema.GroupVersionResource]bool + tweakListOptions TweakListOptionsFunc +} + +var _ SharedInformerFactory = &metadataSharedInformerFactory{} + +func (f *metadataSharedInformerFactory) ForResource(gvr schema.GroupVersionResource) informers.GenericInformer { + f.lock.Lock() + defer f.lock.Unlock() + + key := gvr + informer, exists := f.informers[key] + if exists { + return informer + } + + informer = NewFilteredMetadataInformer(f.client, gvr, f.namespace, f.defaultResync, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) + f.informers[key] = informer + + return informer +} + +// Start initializes all requested informers. +func (f *metadataSharedInformerFactory) Start(stopCh <-chan struct{}) { + f.lock.Lock() + defer f.lock.Unlock() + + for informerType, informer := range f.informers { + if !f.startedInformers[informerType] { + go informer.Informer().Run(stopCh) + f.startedInformers[informerType] = true + } + } +} + +// WaitForCacheSync waits for all started informers' cache were synced. +func (f *metadataSharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool { + informers := func() map[schema.GroupVersionResource]cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informers := map[schema.GroupVersionResource]cache.SharedIndexInformer{} + for informerType, informer := range f.informers { + if f.startedInformers[informerType] { + informers[informerType] = informer.Informer() + } + } + return informers + }() + + res := map[schema.GroupVersionResource]bool{} + for informType, informer := range informers { + res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) + } + return res +} + +// NewFilteredMetadataInformer constructs a new informer for a metadata type. +func NewFilteredMetadataInformer(client metadata.Interface, gvr schema.GroupVersionResource, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions TweakListOptionsFunc) informers.GenericInformer { + return &metadataInformer{ + gvr: gvr, + informer: cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.Resource(gvr).Namespace(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.Resource(gvr).Namespace(namespace).Watch(context.TODO(), options) + }, + }, + &metav1.PartialObjectMetadata{}, + resyncPeriod, + indexers, + ), + } +} + +type metadataInformer struct { + informer cache.SharedIndexInformer + gvr schema.GroupVersionResource +} + +var _ informers.GenericInformer = &metadataInformer{} + +func (d *metadataInformer) Informer() cache.SharedIndexInformer { + return d.informer +} + +func (d *metadataInformer) Lister() cache.GenericLister { + return metadatalister.NewRuntimeObjectShim(metadatalister.New(d.informer.GetIndexer(), d.gvr)) +} diff --git a/vendor/k8s.io/client-go/metadata/metadatainformer/interface.go b/vendor/k8s.io/client-go/metadata/metadatainformer/interface.go new file mode 100644 index 000000000..732e565c7 --- /dev/null +++ b/vendor/k8s.io/client-go/metadata/metadatainformer/interface.go @@ -0,0 +1,34 @@ +/* +Copyright 2018 The Kubernetes 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. +*/ + +package metadatainformer + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/informers" +) + +// SharedInformerFactory provides access to a shared informer and lister for dynamic client +type SharedInformerFactory interface { + Start(stopCh <-chan struct{}) + ForResource(gvr schema.GroupVersionResource) informers.GenericInformer + WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool +} + +// TweakListOptionsFunc defines the signature of a helper function +// that wants to provide more listing options to API +type TweakListOptionsFunc func(*metav1.ListOptions) diff --git a/vendor/k8s.io/client-go/metadata/metadatalister/interface.go b/vendor/k8s.io/client-go/metadata/metadatalister/interface.go new file mode 100644 index 000000000..bb3548589 --- /dev/null +++ b/vendor/k8s.io/client-go/metadata/metadatalister/interface.go @@ -0,0 +1,40 @@ +/* +Copyright 2018 The Kubernetes 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. +*/ + +package metadatalister + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" +) + +// Lister helps list resources. +type Lister interface { + // List lists all resources in the indexer. + List(selector labels.Selector) (ret []*metav1.PartialObjectMetadata, err error) + // Get retrieves a resource from the indexer with the given name + Get(name string) (*metav1.PartialObjectMetadata, error) + // Namespace returns an object that can list and get resources in a given namespace. + Namespace(namespace string) NamespaceLister +} + +// NamespaceLister helps list and get resources. +type NamespaceLister interface { + // List lists all resources in the indexer for a given namespace. + List(selector labels.Selector) (ret []*metav1.PartialObjectMetadata, err error) + // Get retrieves a resource from the indexer for a given namespace and name. + Get(name string) (*metav1.PartialObjectMetadata, error) +} diff --git a/vendor/k8s.io/client-go/metadata/metadatalister/lister.go b/vendor/k8s.io/client-go/metadata/metadatalister/lister.go new file mode 100644 index 000000000..faeccc0fc --- /dev/null +++ b/vendor/k8s.io/client-go/metadata/metadatalister/lister.go @@ -0,0 +1,91 @@ +/* +Copyright 2018 The Kubernetes 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. +*/ + +package metadatalister + +import ( + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/tools/cache" +) + +var _ Lister = &metadataLister{} +var _ NamespaceLister = &metadataNamespaceLister{} + +// metadataLister implements the Lister interface. +type metadataLister struct { + indexer cache.Indexer + gvr schema.GroupVersionResource +} + +// New returns a new Lister. +func New(indexer cache.Indexer, gvr schema.GroupVersionResource) Lister { + return &metadataLister{indexer: indexer, gvr: gvr} +} + +// List lists all resources in the indexer. +func (l *metadataLister) List(selector labels.Selector) (ret []*metav1.PartialObjectMetadata, err error) { + err = cache.ListAll(l.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*metav1.PartialObjectMetadata)) + }) + return ret, err +} + +// Get retrieves a resource from the indexer with the given name +func (l *metadataLister) Get(name string) (*metav1.PartialObjectMetadata, error) { + obj, exists, err := l.indexer.GetByKey(name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(l.gvr.GroupResource(), name) + } + return obj.(*metav1.PartialObjectMetadata), nil +} + +// Namespace returns an object that can list and get resources from a given namespace. +func (l *metadataLister) Namespace(namespace string) NamespaceLister { + return &metadataNamespaceLister{indexer: l.indexer, namespace: namespace, gvr: l.gvr} +} + +// metadataNamespaceLister implements the NamespaceLister interface. +type metadataNamespaceLister struct { + indexer cache.Indexer + namespace string + gvr schema.GroupVersionResource +} + +// List lists all resources in the indexer for a given namespace. +func (l *metadataNamespaceLister) List(selector labels.Selector) (ret []*metav1.PartialObjectMetadata, err error) { + err = cache.ListAllByNamespace(l.indexer, l.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*metav1.PartialObjectMetadata)) + }) + return ret, err +} + +// Get retrieves a resource from the indexer for a given namespace and name. +func (l *metadataNamespaceLister) Get(name string) (*metav1.PartialObjectMetadata, error) { + obj, exists, err := l.indexer.GetByKey(l.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(l.gvr.GroupResource(), name) + } + return obj.(*metav1.PartialObjectMetadata), nil +} diff --git a/vendor/k8s.io/client-go/metadata/metadatalister/shim.go b/vendor/k8s.io/client-go/metadata/metadatalister/shim.go new file mode 100644 index 000000000..f31c60725 --- /dev/null +++ b/vendor/k8s.io/client-go/metadata/metadatalister/shim.go @@ -0,0 +1,87 @@ +/* +Copyright 2018 The Kubernetes 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. +*/ + +package metadatalister + +import ( + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/cache" +) + +var _ cache.GenericLister = &metadataListerShim{} +var _ cache.GenericNamespaceLister = &metadataNamespaceListerShim{} + +// metadataListerShim implements the cache.GenericLister interface. +type metadataListerShim struct { + lister Lister +} + +// NewRuntimeObjectShim returns a new shim for Lister. +// It wraps Lister so that it implements cache.GenericLister interface +func NewRuntimeObjectShim(lister Lister) cache.GenericLister { + return &metadataListerShim{lister: lister} +} + +// List will return all objects across namespaces +func (s *metadataListerShim) List(selector labels.Selector) (ret []runtime.Object, err error) { + objs, err := s.lister.List(selector) + if err != nil { + return nil, err + } + + ret = make([]runtime.Object, len(objs)) + for index, obj := range objs { + ret[index] = obj + } + return ret, err +} + +// Get will attempt to retrieve assuming that name==key +func (s *metadataListerShim) Get(name string) (runtime.Object, error) { + return s.lister.Get(name) +} + +func (s *metadataListerShim) ByNamespace(namespace string) cache.GenericNamespaceLister { + return &metadataNamespaceListerShim{ + namespaceLister: s.lister.Namespace(namespace), + } +} + +// metadataNamespaceListerShim implements the NamespaceLister interface. +// It wraps NamespaceLister so that it implements cache.GenericNamespaceLister interface +type metadataNamespaceListerShim struct { + namespaceLister NamespaceLister +} + +// List will return all objects in this namespace +func (ns *metadataNamespaceListerShim) List(selector labels.Selector) (ret []runtime.Object, err error) { + objs, err := ns.namespaceLister.List(selector) + if err != nil { + return nil, err + } + + ret = make([]runtime.Object, len(objs)) + for index, obj := range objs { + ret[index] = obj + } + return ret, err +} + +// Get will attempt to retrieve by namespace and name +func (ns *metadataNamespaceListerShim) Get(name string) (runtime.Object, error) { + return ns.namespaceLister.Get(name) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index af18ba87e..7e42a3498 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1424,6 +1424,8 @@ k8s.io/client-go/listers/storage/v1 k8s.io/client-go/listers/storage/v1alpha1 k8s.io/client-go/listers/storage/v1beta1 k8s.io/client-go/metadata +k8s.io/client-go/metadata/metadatainformer +k8s.io/client-go/metadata/metadatalister k8s.io/client-go/pkg/apis/clientauthentication k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1 k8s.io/client-go/pkg/apis/clientauthentication/v1beta1 From 298a5d38278caad0b1e3f976394d163493ebbda4 Mon Sep 17 00:00:00 2001 From: "Roland.Ma" Date: Tue, 2 Mar 2021 05:35:05 +0000 Subject: [PATCH 17/21] fix update vendor script Signed-off-by: Roland.Ma --- hack/update-vendor.sh | 7 +++++++ vendor/kubesphere.io/client-go | 1 + 2 files changed, 8 insertions(+) create mode 120000 vendor/kubesphere.io/client-go diff --git a/hack/update-vendor.sh b/hack/update-vendor.sh index 0af9ce8db..b68887f43 100755 --- a/hack/update-vendor.sh +++ b/hack/update-vendor.sh @@ -184,5 +184,12 @@ go mod vendor >>"${LOG_FILE}" 2>&1 awk '{if($1=="#") print $2 " " $0; else print}' < vendor/modules.txt | sort -k1,1 -s | sed 's/.*#/#/' > "${TMP_DIR}/modules.txt.tmp" mv "${TMP_DIR}/modules.txt.tmp" vendor/modules.txt +# create a symlink in vendor directory pointing to the staging components. +# This lets other packages and tools use the local staging components as if they were vendored. +for repo in $(kube::util::list_staging_repos); do + rm -fr "${KUBE_ROOT}/vendor/kubesphere.io/${repo}" + ln -s "../../staging/src/kubesphere.io/${repo}" "${KUBE_ROOT}/vendor/kubesphere.io/${repo}" +done + #kube::log::status "vendor: updating LICENSES file" #hack/update-vendor-licenses.sh >>"${LOG_FILE}" 2>&1 diff --git a/vendor/kubesphere.io/client-go b/vendor/kubesphere.io/client-go new file mode 120000 index 000000000..0bee05eff --- /dev/null +++ b/vendor/kubesphere.io/client-go @@ -0,0 +1 @@ +../../staging/src/kubesphere.io/client-go \ No newline at end of file From 921341d8ce5b0072bdf229348fb8b137051c98fd Mon Sep 17 00:00:00 2001 From: shaowenchen Date: Tue, 2 Mar 2021 15:11:12 +0800 Subject: [PATCH 18/21] fix: pipeline sync annotations Signed-off-by: shaowenchen --- pkg/kapis/devops/v1alpha2/devops.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/kapis/devops/v1alpha2/devops.go b/pkg/kapis/devops/v1alpha2/devops.go index d845e0b52..c8039bd19 100644 --- a/pkg/kapis/devops/v1alpha2/devops.go +++ b/pkg/kapis/devops/v1alpha2/devops.go @@ -20,6 +20,9 @@ import ( "encoding/json" "errors" "fmt" + "net/http" + "strings" + "github.com/emicklei/go-restful" "k8s.io/apiserver/pkg/authentication/user" log "k8s.io/klog" @@ -33,8 +36,6 @@ import ( "kubesphere.io/kubesphere/pkg/server/params" clientDevOps "kubesphere.io/kubesphere/pkg/simple/client/devops" "kubesphere.io/kubesphere/pkg/simple/client/devops/jenkins" - "net/http" - "strings" ) const jenkinsHeaderPre = "X-" @@ -131,7 +132,10 @@ func (h *ProjectPipelineHandler) ListPipelines(req *restful.Request, resp *restf } else { for i, _ := range res.Items { if index, ok := pipelineMap[res.Items[i].Name]; ok { + // keep annotations field of pipelineList + annotations := pipelineList.Items[index].Annotations pipelineList.Items[index] = res.Items[i] + pipelineList.Items[index].Annotations = annotations } } } From e058f938d14565d061cf35df3ac86fa24f6110a0 Mon Sep 17 00:00:00 2001 From: "Roland.Ma" Date: Thu, 4 Mar 2021 06:51:20 +0000 Subject: [PATCH 19/21] staging repo sync rules Signed-off-by: Roland.Ma --- staging/README.md | 60 +++++++++++++++++++++++++++++++++++ staging/publishing/rules.yaml | 21 ++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 staging/README.md create mode 100644 staging/publishing/rules.yaml diff --git a/staging/README.md b/staging/README.md new file mode 100644 index 000000000..7768b58cd --- /dev/null +++ b/staging/README.md @@ -0,0 +1,60 @@ +# External Repository Staging Area + +This directory is the staging area for packages that have been split to their +own repository. The content here will be periodically published to respective +top-level kubesphere.io repositories. + +Repositories currently staged here: + +- [`kubesphere.io/client-go`](https://github.com/kubesphere/client-go) + + +The code in the staging/ directory is authoritative, i.e. the only copy of the +code. You can directly modify such code. + +## Using staged repositories from KubeSphere code + +KubeSphere code uses the repositories in this directory via symlinks in the +`vendor/kubesphere.io` directory into this staging area. For example, when +KubeSphere code imports a package from the `kubesphere.io/client-go` repository, that +import is resolved to `staging/src/kubesphere.io/client-go` relative to the project +root: + +```go +// pkg/example/some_code.go +package example + +import ( + "kubesphere.io/client-go/" // resolves to staging/src/kubesphere.io/client-go/dynamic +) +``` + +Once the change-over to external repositories is complete, these repositories +will actually be vendored from `kubesphere.io/`. + +## Creating a new repository in staging + +### Adding the staging repository in `kubesphere/kubesphere`: + +1. Add a propose to sig-architecture in [community](https://github.com/kubesphere/community/). Waiting approval for creating the staging repository. + +2. Once approval has been granted, create the new staging repository. + +3. Add a symlink to the staging repo in `vendor/kubesphere.io`. + +4. Add all mandatory template files to the staging repo such as README.md, LICENSE, OWNER,CONTRIBUTING.md. + + +### Creating the published repository + +1. Create an repository in the KubeSphere org. The published repository **must** have an +initial empty commit. + +2. Setup branch protection and enable access to the `ks-publishing-bot` bot. + +3. Once the repository has been created in the KubeSphere org, update the publishing-bot to publish the staging repository by updating: + + - [`rules.yaml`](/staging/publishing/rules.yaml): + Make sure that the list of dependencies reflects the staging repos in the `Godeps.json` file. + +4. Add the repo to the list of staging repos in this `README.md` file. \ No newline at end of file diff --git a/staging/publishing/rules.yaml b/staging/publishing/rules.yaml new file mode 100644 index 000000000..ddd51301c --- /dev/null +++ b/staging/publishing/rules.yaml @@ -0,0 +1,21 @@ +recursive-delete-patterns: +- BUILD +default-go-version: 1.13.15 +rules: +- destination: client-go + library: true + branches: + - source: + branch: master + dir: staging/src/kubesphere.io/client-go + name: master + # - source: + # branch: release-3.1 + # dir: staging/src/kubesphere.io/client-go + # name: release-3.1 + # go: 1.13.15 + + smoke-test: | + # assumes GO111MODULE=on + go build ./... + go test ./... \ No newline at end of file From fe2f2172ba8996d78bfde568958380aa82a1c44f Mon Sep 17 00:00:00 2001 From: rick <1450685+LinuxSuRen@users.noreply.github.com> Date: Thu, 4 Mar 2021 16:32:33 +0800 Subject: [PATCH 20/21] Fix unmarshal bool into Go struct due to the boolean parameter Signed-off-by: rick <1450685+LinuxSuRen@users.noreply.github.com> --- pkg/simple/client/devops/pipeline.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/simple/client/devops/pipeline.go b/pkg/simple/client/devops/pipeline.go index 00e7c5405..98b4a0908 100644 --- a/pkg/simple/client/devops/pipeline.go +++ b/pkg/simple/client/devops/pipeline.go @@ -504,9 +504,9 @@ type PipelineBranchItem struct { Parameters []struct { Class string `json:"_class,omitempty" description:"It’s a fully qualified name and is an identifier of the producer of this resource's capability."` DefaultParameterValue struct { - Class string `json:"_class,omitempty" description:"It’s a fully qualified name and is an identifier of the producer of this resource's capability."` - Name string `json:"name,omitempty" description:"name"` - Value string `json:"value,omitempty" description:"value"` + Class string `json:"_class,omitempty" description:"It’s a fully qualified name and is an identifier of the producer of this resource's capability."` + Name string `json:"name,omitempty" description:"name"` + Value interface{} `json:"value,omitempty" description:"value"` } `json:"defaultParameterValue,omitempty"` Description string `json:"description,omitempty" description:"description"` Name string `json:"name,omitempty" description:"name"` @@ -536,8 +536,8 @@ type PipelineBranchItem struct { // RunPipeline type RunPayload struct { Parameters []struct { - Name string `json:"name,omitempty" description:"name"` - Value string `json:"value,omitempty" description:"value"` + Name string `json:"name,omitempty" description:"name"` + Value interface{} `json:"value,omitempty" description:"value"` } `json:"parameters,omitempty"` } @@ -1036,8 +1036,8 @@ type ResJson struct { Arguments []struct { Key string `json:"key,omitempty" description:"key"` Value struct { - IsLiteral bool `json:"isLiteral,omitempty" description:"is literal or not"` - Value string `json:"value,omitempty" description:"value"` + IsLiteral bool `json:"isLiteral,omitempty" description:"is literal or not"` + Value interface{} `json:"value,omitempty" description:"value"` } `json:"value,omitempty"` } `json:"arguments,omitempty"` } `json:"parameters,omitempty"` From 67cbff464ff22a1a332a30db78c05f21634d6733 Mon Sep 17 00:00:00 2001 From: Duan Jiong Date: Thu, 25 Feb 2021 18:53:08 +0800 Subject: [PATCH 21/21] fix ippool status statistics and delete ippool label while workspace is deleted sync default ippool to namespace annotation Signed-off-by: Duan Jiong --- cmd/controller-manager/app/controllers.go | 8 +- .../network/ippool/ippool_controller.go | 265 ++++++++++++++---- .../network/ippool/ippool_controller_test.go | 17 +- .../client/network/ippool/calico/provider.go | 244 ++++++++-------- pkg/simple/client/network/ippool/ipam/ipam.go | 4 + pkg/simple/client/network/ippool/provider.go | 12 +- 6 files changed, 368 insertions(+), 182 deletions(-) diff --git a/cmd/controller-manager/app/controllers.go b/cmd/controller-manager/app/controllers.go index 67d009e76..27e1413b4 100644 --- a/cmd/controller-manager/app/controllers.go +++ b/cmd/controller-manager/app/controllers.go @@ -253,13 +253,9 @@ func addControllers( } var ippoolController manager.Runnable - ippoolProvider := ippoolclient.NewProvider(kubernetesInformer.Core().V1().Pods(), client.KubeSphere(), client.Kubernetes(), networkOptions.IPPoolType, options) + ippoolProvider := ippoolclient.NewProvider(kubernetesInformer, client.KubeSphere(), client.Kubernetes(), networkOptions.IPPoolType, options) if ippoolProvider != nil { - ippoolController = ippool.NewIPPoolController(kubesphereInformer.Network().V1alpha1().IPPools(), - kubesphereInformer.Network().V1alpha1().IPAMBlocks(), - client.Kubernetes(), - client.KubeSphere(), - ippoolProvider) + ippoolController = ippool.NewIPPoolController(kubesphereInformer, kubernetesInformer, client.Kubernetes(), client.KubeSphere(), ippoolProvider) } controllers := map[string]manager.Runnable{ diff --git a/pkg/controller/network/ippool/ippool_controller.go b/pkg/controller/network/ippool/ippool_controller.go index bbf1fe7ce..24b3a5df5 100644 --- a/pkg/controller/network/ippool/ippool_controller.go +++ b/pkg/controller/network/ippool/ippool_controller.go @@ -19,32 +19,36 @@ package ippool import ( "context" "fmt" - "reflect" - "time" - cnet "github.com/projectcalico/libcalico-go/lib/net" - podv1 "k8s.io/api/core/v1" - v1 "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" + k8sinformers "k8s.io/client-go/informers" + coreinfomers "k8s.io/client-go/informers/core/v1" clientset "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" - corev1 "k8s.io/client-go/kubernetes/typed/core/v1" + clientcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" "k8s.io/klog" networkv1alpha1 "kubesphere.io/kubesphere/pkg/apis/network/v1alpha1" + tenantv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1" kubesphereclient "kubesphere.io/kubesphere/pkg/client/clientset/versioned" + ksinformers "kubesphere.io/kubesphere/pkg/client/informers/externalversions" networkInformer "kubesphere.io/kubesphere/pkg/client/informers/externalversions/network/v1alpha1" + tenantv1alpha1informers "kubesphere.io/kubesphere/pkg/client/informers/externalversions/tenant/v1alpha1" + "kubesphere.io/kubesphere/pkg/constants" "kubesphere.io/kubesphere/pkg/controller/network/utils" "kubesphere.io/kubesphere/pkg/controller/network/webhooks" "kubesphere.io/kubesphere/pkg/simple/client/network/ippool" + "reflect" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "time" ) var ( @@ -61,6 +65,13 @@ type IPPoolController struct { ippoolSynced cache.InformerSynced ippoolQueue workqueue.RateLimitingInterface + wsInformer tenantv1alpha1informers.WorkspaceInformer + wsSynced cache.InformerSynced + + nsInformer coreinfomers.NamespaceInformer + nsSynced cache.InformerSynced + nsQueue workqueue.RateLimitingInterface + ipamblockInformer networkInformer.IPAMBlockInformer ipamblockSynced cache.InformerSynced @@ -68,31 +79,25 @@ type IPPoolController struct { kubesphereClient kubesphereclient.Interface } -func (c *IPPoolController) ippoolHandle(obj interface{}) { +func (c *IPPoolController) enqueueIPPools(obj interface{}) { pool, ok := obj.(*networkv1alpha1.IPPool) if !ok { utilruntime.HandleError(fmt.Errorf("IPPool informer returned non-ippool object: %#v", obj)) return } - key, err := cache.MetaNamespaceKeyFunc(pool) - if err != nil { - utilruntime.HandleError(fmt.Errorf("couldn't get key for ippool %#v: %v", pool, err)) - return - } - if utils.NeedToAddFinalizer(pool, networkv1alpha1.IPPoolFinalizer) || utils.IsDeletionCandidate(pool, networkv1alpha1.IPPoolFinalizer) { - c.ippoolQueue.Add(key) - } + c.ippoolQueue.Add(pool.Name) } func (c *IPPoolController) addFinalizer(pool *networkv1alpha1.IPPool) error { clone := pool.DeepCopy() controllerutil.AddFinalizer(clone, networkv1alpha1.IPPoolFinalizer) - clone.Labels = map[string]string{ - networkv1alpha1.IPPoolNameLabel: clone.Name, - networkv1alpha1.IPPoolTypeLabel: clone.Spec.Type, - networkv1alpha1.IPPoolIDLabel: fmt.Sprintf("%d", clone.ID()), + if clone.Labels == nil { + clone.Labels = make(map[string]string) } + clone.Labels[networkv1alpha1.IPPoolNameLabel] = clone.Name + clone.Labels[networkv1alpha1.IPPoolTypeLabel] = clone.Spec.Type + clone.Labels[networkv1alpha1.IPPoolIDLabel] = fmt.Sprintf("%d", clone.ID()) pool, err := c.kubesphereClient.NetworkV1alpha1().IPPools().Update(context.TODO(), clone, metav1.UpdateOptions{}) if err != nil { klog.V(3).Infof("Error adding finalizer to pool %s: %v", pool.Name, err) @@ -116,12 +121,15 @@ func (c *IPPoolController) removeFinalizer(pool *networkv1alpha1.IPPool) error { func (c *IPPoolController) ValidateCreate(obj runtime.Object) error { b := obj.(*networkv1alpha1.IPPool) - _, cidr, err := cnet.ParseCIDR(b.Spec.CIDR) + ip, cidr, err := cnet.ParseCIDR(b.Spec.CIDR) if err != nil { return fmt.Errorf("invalid cidr") } size, _ := cidr.Mask.Size() + if ip.IP.To4() != nil && size == 32 { + return fmt.Errorf("the cidr mask must be less than 32") + } if b.Spec.BlockSize > 0 && b.Spec.BlockSize < size { return fmt.Errorf("the blocksize should be larger than the cidr mask") } @@ -163,6 +171,25 @@ func (c *IPPoolController) ValidateCreate(obj runtime.Object) error { return nil } +func (c *IPPoolController) validateDefaultIPPool(p *networkv1alpha1.IPPool) error { + pools, err := c.kubesphereClient.NetworkV1alpha1().IPPools().List(context.TODO(), metav1.ListOptions{ + LabelSelector: labels.SelectorFromSet( + labels.Set{ + networkv1alpha1.IPPoolDefaultLabel: "", + }).String(), + }) + if err != nil { + return err + } + + poolLen := len(pools.Items) + if poolLen != 1 || pools.Items[0].Name != p.Name { + return nil + } + + return fmt.Errorf("Must ensure that there is at least one default ippool") +} + func (c *IPPoolController) ValidateUpdate(old runtime.Object, new runtime.Object) error { oldP := old.(*networkv1alpha1.IPPool) newP := new.(*networkv1alpha1.IPPool) @@ -183,6 +210,15 @@ func (c *IPPoolController) ValidateUpdate(old runtime.Object, new runtime.Object return fmt.Errorf("ippool rangeEnd/rangeStart cannot be modified") } + _, defaultOld := oldP.Labels[networkv1alpha1.IPPoolDefaultLabel] + _, defaultNew := newP.Labels[networkv1alpha1.IPPoolDefaultLabel] + if !defaultNew && defaultOld != defaultNew { + err := c.validateDefaultIPPool(newP) + if err != nil { + return err + } + } + return nil } @@ -193,7 +229,7 @@ func (c *IPPoolController) ValidateDelete(obj runtime.Object) error { return fmt.Errorf("ippool is in use, please remove the workload before deleting") } - return nil + return c.validateDefaultIPPool(p) } func (c *IPPoolController) disableIPPool(old *networkv1alpha1.IPPool) error { @@ -204,7 +240,7 @@ func (c *IPPoolController) disableIPPool(old *networkv1alpha1.IPPool) error { clone := old.DeepCopy() clone.Spec.Disabled = true - old, err := c.kubesphereClient.NetworkV1alpha1().IPPools().Update(context.TODO(), clone, metav1.UpdateOptions{}) + _, err := c.kubesphereClient.NetworkV1alpha1().IPPools().Update(context.TODO(), clone, metav1.UpdateOptions{}) return err } @@ -305,19 +341,20 @@ func (c *IPPoolController) Run(workers int, stopCh <-chan struct{}) error { klog.Info("starting ippool controller") defer klog.Info("shutting down ippool controller") - if !cache.WaitForCacheSync(stopCh, c.ippoolSynced, c.ipamblockSynced) { + if !cache.WaitForCacheSync(stopCh, c.ippoolSynced, c.ipamblockSynced, c.wsSynced, c.nsSynced) { return fmt.Errorf("failed to wait for caches to sync") } for i := 0; i < workers; i++ { - go wait.Until(c.runWorker, time.Second, stopCh) + go wait.Until(c.runIPPoolWorker, time.Second, stopCh) + go wait.Until(c.runNSWorker, time.Second, stopCh) } <-stopCh return nil } -func (c *IPPoolController) runWorker() { +func (c *IPPoolController) runIPPoolWorker() { for c.processIPPoolItem() { } } @@ -329,13 +366,7 @@ func (c *IPPoolController) processIPPoolItem() bool { } defer c.ippoolQueue.Done(key) - _, name, err := cache.SplitMetaNamespaceKey(key.(string)) - if err != nil { - utilruntime.HandleError(fmt.Errorf("error parsing ippool key %q: %v", key, err)) - return true - } - - delay, err := c.processIPPool(name) + delay, err := c.processIPPool(key.(string)) if err == nil { c.ippoolQueue.Forget(key) return true @@ -350,7 +381,63 @@ func (c *IPPoolController) processIPPoolItem() bool { return true } -func (c *IPPoolController) ipamblockHandle(obj interface{}) { +func (c *IPPoolController) runNSWorker() { + for c.processNSItem() { + } +} + +func (c *IPPoolController) processNS(name string) error { + ns, err := c.nsInformer.Lister().Get(name) + if apierrors.IsNotFound(err) { + return nil + } + + var poolsName []string + if ns.Labels != nil && ns.Labels[constants.WorkspaceLabelKey] != "" { + pools, err := c.ippoolInformer.Lister().List(labels.SelectorFromSet(labels.Set{ + networkv1alpha1.IPPoolDefaultLabel: "", + })) + if err != nil { + return err + } + + for _, pool := range pools { + poolsName = append(poolsName, pool.Name) + } + } + + clone := ns.DeepCopy() + err = c.provider.UpdateNamespace(clone, poolsName) + if err != nil { + return err + } + if reflect.DeepEqual(clone, ns) { + return nil + } + + _, err = c.client.CoreV1().Namespaces().Update(context.TODO(), clone, metav1.UpdateOptions{}) + return err +} + +func (c *IPPoolController) processNSItem() bool { + key, quit := c.nsQueue.Get() + if quit { + return false + } + defer c.nsQueue.Done(key) + + err := c.processNS(key.(string)) + if err == nil { + c.nsQueue.Forget(key) + return true + } + + c.nsQueue.AddRateLimited(key) + utilruntime.HandleError(fmt.Errorf("error processing ns %v (will retry): %v", key, err)) + return true +} + +func (c *IPPoolController) enqueueIPAMBlocks(obj interface{}) { block, ok := obj.(*networkv1alpha1.IPAMBlock) if !ok { return @@ -360,9 +447,47 @@ func (c *IPPoolController) ipamblockHandle(obj interface{}) { c.ippoolQueue.Add(poolName) } +func (c *IPPoolController) enqueueWorkspace(obj interface{}) { + wk, ok := obj.(*tenantv1alpha1.Workspace) + if !ok { + return + } + + pools, err := c.ippoolInformer.Lister().List(labels.SelectorFromSet(labels.Set{ + constants.WorkspaceLabelKey: wk.Name, + })) + if err != nil { + klog.Errorf("failed to list ippools by worksapce %s, err=%v", wk.Name, err) + } + + for _, pool := range pools { + c.ippoolQueue.Add(pool.Name) + } +} + +func (c *IPPoolController) enqueueNamespace(old interface{}, new interface{}) { + workspaceOld := "" + if old != nil { + nsOld := old.(*corev1.Namespace) + if nsOld.Labels != nil { + workspaceOld = nsOld.Labels[constants.WorkspaceLabelKey] + } + } + + nsNew := new.(*corev1.Namespace) + workspaceNew := "" + if nsNew.Labels != nil { + workspaceNew = nsNew.Labels[constants.WorkspaceLabelKey] + } + + if workspaceOld != workspaceNew { + c.nsQueue.Add(nsNew.Name) + } +} + func NewIPPoolController( - ippoolInformer networkInformer.IPPoolInformer, - ipamblockInformer networkInformer.IPAMBlockInformer, + kubesphereInformers ksinformers.SharedInformerFactory, + kubernetesInformers k8sinformers.SharedInformerFactory, client clientset.Interface, kubesphereClient kubesphereclient.Interface, provider ippool.Provider) *IPPoolController { @@ -371,43 +496,71 @@ func NewIPPoolController( broadcaster.StartLogging(func(format string, args ...interface{}) { klog.Info(fmt.Sprintf(format, args)) }) - broadcaster.StartRecordingToSink(&corev1.EventSinkImpl{Interface: client.CoreV1().Events("")}) - recorder := broadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "ippool-controller"}) + broadcaster.StartRecordingToSink(&clientcorev1.EventSinkImpl{Interface: client.CoreV1().Events("")}) + recorder := broadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "ippool-controller"}) c := &IPPoolController{ - eventBroadcaster: broadcaster, - eventRecorder: recorder, - ippoolInformer: ippoolInformer, - ippoolSynced: ippoolInformer.Informer().HasSynced, - ippoolQueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "ippool"), - ipamblockInformer: ipamblockInformer, - ipamblockSynced: ipamblockInformer.Informer().HasSynced, - client: client, - kubesphereClient: kubesphereClient, - provider: provider, + eventBroadcaster: broadcaster, + eventRecorder: recorder, + ippoolQueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "ippool"), + nsQueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "ippool-ns"), + client: client, + kubesphereClient: kubesphereClient, + provider: provider, } + c.ippoolInformer = kubesphereInformers.Network().V1alpha1().IPPools() + c.ippoolSynced = c.ippoolInformer.Informer().HasSynced + c.ipamblockInformer = kubesphereInformers.Network().V1alpha1().IPAMBlocks() + c.ipamblockSynced = c.ipamblockInformer.Informer().HasSynced + c.wsInformer = kubesphereInformers.Tenant().V1alpha1().Workspaces() + c.wsSynced = c.wsInformer.Informer().HasSynced + c.nsInformer = kubernetesInformers.Core().V1().Namespaces() + c.nsSynced = c.nsInformer.Informer().HasSynced - ippoolInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ - AddFunc: c.ippoolHandle, + c.ippoolInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: c.enqueueIPPools, UpdateFunc: func(old, new interface{}) { - c.ippoolHandle(new) + _, defaultOld := old.(*networkv1alpha1.IPPool).Labels[networkv1alpha1.IPPoolDefaultLabel] + _, defaultNew := new.(*networkv1alpha1.IPPool).Labels[networkv1alpha1.IPPoolDefaultLabel] + if defaultOld != defaultNew { + nss, err := c.nsInformer.Lister().List(labels.Everything()) + if err != nil { + return + } + + for _, ns := range nss { + c.enqueueNamespace(nil, ns) + } + } + c.enqueueIPPools(new) }, }) //just for update ippool status - ipamblockInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ - AddFunc: c.ipamblockHandle, + c.ipamblockInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: c.enqueueIPAMBlocks, UpdateFunc: func(old, new interface{}) { - c.ipamblockHandle(new) + c.enqueueIPAMBlocks(new) }, - DeleteFunc: c.ipamblockHandle, + DeleteFunc: c.enqueueIPAMBlocks, + }) + + c.wsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + DeleteFunc: c.enqueueWorkspace, + }) + + c.nsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(new interface{}) { + c.enqueueNamespace(nil, new) + }, + UpdateFunc: c.enqueueNamespace, }) //register ippool webhook webhooks.RegisterValidator(networkv1alpha1.SchemeGroupVersion.WithKind(networkv1alpha1.ResourceKindIPPool).String(), &webhooks.ValidatorWrap{Obj: &networkv1alpha1.IPPool{}, Helper: c}) - webhooks.RegisterDefaulter(podv1.SchemeGroupVersion.WithKind("Pod").String(), - &webhooks.DefaulterWrap{Obj: &podv1.Pod{}, Helper: provider}) + webhooks.RegisterDefaulter(corev1.SchemeGroupVersion.WithKind("Pod").String(), + &webhooks.DefaulterWrap{Obj: &corev1.Pod{}, Helper: provider}) return c } diff --git a/pkg/controller/network/ippool/ippool_controller_test.go b/pkg/controller/network/ippool/ippool_controller_test.go index c3dc4430c..6740af473 100644 --- a/pkg/controller/network/ippool/ippool_controller_test.go +++ b/pkg/controller/network/ippool/ippool_controller_test.go @@ -25,6 +25,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sinformers "k8s.io/client-go/informers" k8sfake "k8s.io/client-go/kubernetes/fake" "k8s.io/klog" "kubesphere.io/kubesphere/pkg/apis/network/v1alpha1" @@ -45,6 +46,10 @@ func TestIPPoolSuit(t *testing.T) { RunSpecs(t, "IPPool Suite") } +var ( + alwaysReady = func() bool { return true } +) + var _ = Describe("test ippool", func() { pool := &v1alpha1.IPPool{ TypeMeta: v1.TypeMeta{}, @@ -60,16 +65,16 @@ var _ = Describe("test ippool", func() { ksclient := ksfake.NewSimpleClientset() k8sclinet := k8sfake.NewSimpleClientset() - p := ippool.NewProvider(nil, ksclient, k8sclinet, v1alpha1.IPPoolTypeLocal, nil) - ipamClient := ipam.NewIPAMClient(ksclient, v1alpha1.VLAN) - ksInformer := ksinformers.NewSharedInformerFactory(ksclient, 0) - ippoolInformer := ksInformer.Network().V1alpha1().IPPools() - ipamblockInformer := ksInformer.Network().V1alpha1().IPAMBlocks() - c := NewIPPoolController(ippoolInformer, ipamblockInformer, k8sclinet, ksclient, p) + k8sInformer := k8sinformers.NewSharedInformerFactory(k8sclinet, 0) + + p := ippool.NewProvider(k8sInformer, ksclient, k8sclinet, v1alpha1.IPPoolTypeLocal, nil) + ipamClient := ipam.NewIPAMClient(ksclient, v1alpha1.VLAN) + c := NewIPPoolController(ksInformer, k8sInformer, k8sclinet, ksclient, p) stopCh := make(chan struct{}) go ksInformer.Start(stopCh) + go k8sInformer.Start(stopCh) go c.Start(stopCh) It("test create ippool", func() { diff --git a/pkg/simple/client/network/ippool/calico/provider.go b/pkg/simple/client/network/ippool/calico/provider.go index b4d622208..e3081da7c 100644 --- a/pkg/simple/client/network/ippool/calico/provider.go +++ b/pkg/simple/client/network/ippool/calico/provider.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "fmt" + k8sinformers "k8s.io/client-go/informers" "net" "time" @@ -38,7 +39,6 @@ import ( clientset "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/util/retry" "k8s.io/client-go/util/workqueue" "k8s.io/klog" "kubesphere.io/kubesphere/pkg/apis/network/calicov3" @@ -94,7 +94,7 @@ type provider struct { options Options } -func (c provider) CreateIPPool(pool *v1alpha1.IPPool) error { +func (p *provider) CreateIPPool(pool *v1alpha1.IPPool) error { calicoPool := &calicov3.IPPool{ TypeMeta: v1.TypeMeta{}, ObjectMeta: v1.ObjectMeta{ @@ -104,9 +104,9 @@ func (c provider) CreateIPPool(pool *v1alpha1.IPPool) error { CIDR: pool.Spec.CIDR, Disabled: pool.Spec.Disabled, NodeSelector: "all()", - VXLANMode: v3.VXLANMode(c.options.VXLANMode), - IPIPMode: v3.IPIPMode(c.options.IPIPMode), - NATOutgoing: c.options.NATOutgoing, + VXLANMode: v3.VXLANMode(p.options.VXLANMode), + IPIPMode: v3.IPIPMode(p.options.IPIPMode), + NATOutgoing: p.options.NATOutgoing, }, } @@ -121,7 +121,7 @@ func (c provider) CreateIPPool(pool *v1alpha1.IPPool) error { klog.Warningf("cannot set reference for calico ippool %s, err=%v", pool.Name, err) } - _, err = c.client.CrdCalicov3().IPPools().Create(context.TODO(), calicoPool, v1.CreateOptions{}) + _, err = p.client.CrdCalicov3().IPPools().Create(context.TODO(), calicoPool, v1.CreateOptions{}) if k8serrors.IsAlreadyExists(err) { return nil } @@ -129,19 +129,22 @@ func (c provider) CreateIPPool(pool *v1alpha1.IPPool) error { return err } -func (c provider) UpdateIPPool(pool *v1alpha1.IPPool) error { +func (p *provider) UpdateIPPool(pool *v1alpha1.IPPool) error { return nil } -func (c provider) GetIPPoolStats(pool *v1alpha1.IPPool) (*v1alpha1.IPPool, error) { +func (p *provider) GetIPPoolStats(pool *v1alpha1.IPPool) (*v1alpha1.IPPool, error) { stats := pool.DeepCopy() - calicoPool, err := c.client.CrdCalicov3().IPPools().Get(context.TODO(), pool.Name, v1.GetOptions{}) + calicoPool, err := p.client.CrdCalicov3().IPPools().Get(context.TODO(), pool.Name, v1.GetOptions{}) if err != nil { return nil, err } - blocks, err := c.listBlocks(calicoPool) + blocks, err := p.block.Lister().List(labels.SelectorFromSet( + labels.Set{ + v1alpha1.IPPoolNameLabel: calicoPool.Name, + })) if err != nil { return nil, err } @@ -152,9 +155,7 @@ func (c provider) GetIPPoolStats(pool *v1alpha1.IPPool) (*v1alpha1.IPPool, error stats.Status.Synced = true stats.Status.Allocations = 0 stats.Status.Reserved = 0 - if stats.Status.Workspaces == nil { - stats.Status.Workspaces = make(map[string]v1alpha1.WorkspaceStatus) - } + stats.Status.Workspaces = make(map[string]v1alpha1.WorkspaceStatus) if len(blocks) <= 0 { stats.Status.Unallocated = pool.NumAddresses() @@ -168,23 +169,20 @@ func (c provider) GetIPPoolStats(pool *v1alpha1.IPPool) (*v1alpha1.IPPool, error stats.Status.Unallocated = stats.Status.Capacity - stats.Status.Allocations - stats.Status.Reserved } - wks, err := c.getAssociatedWorkspaces(pool) + wks, err := p.getAssociatedWorkspaces(pool) if err != nil { return nil, err } for _, wk := range wks { - status, err := c.getWorkspaceStatus(wk, pool.GetName()) + status, err := p.getWorkspaceStatus(wk, pool.GetName()) if err != nil { return nil, err } - stats.Status.Workspaces[wk] = *status - } - - for name, wk := range stats.Status.Workspaces { - if wk.Allocations == 0 { - delete(stats.Status.Workspaces, name) + if status.Allocations == 0 { + continue } + stats.Status.Workspaces[wk] = *status } return stats, nil @@ -195,16 +193,18 @@ func setBlockAffiDeletion(c calicoset.Interface, blockAffi *calicov3.BlockAffini return nil } - blockAffi.Spec.State = string(model.StatePendingDeletion) - _, err := c.CrdCalicov3().BlockAffinities().Update(context.TODO(), blockAffi, v1.UpdateOptions{}) + clone := blockAffi.DeepCopy() + clone.Spec.State = string(model.StatePendingDeletion) + _, err := c.CrdCalicov3().BlockAffinities().Update(context.TODO(), clone, v1.UpdateOptions{}) return err } func deleteBlockAffi(c calicoset.Interface, blockAffi *calicov3.BlockAffinity) error { trueStr := fmt.Sprintf("%t", true) if blockAffi.Spec.Deleted != trueStr { - blockAffi.Spec.Deleted = trueStr - _, err := c.CrdCalicov3().BlockAffinities().Update(context.TODO(), blockAffi, v1.UpdateOptions{}) + clone := blockAffi.DeepCopy() + clone.Spec.Deleted = trueStr + _, err := c.CrdCalicov3().BlockAffinities().Update(context.TODO(), clone, v1.UpdateOptions{}) if err != nil { return err } @@ -218,10 +218,10 @@ func deleteBlockAffi(c calicoset.Interface, blockAffi *calicov3.BlockAffinity) e return nil } -func (c provider) doBlockAffis(pool *calicov3.IPPool, do func(calicoset.Interface, *calicov3.BlockAffinity) error) error { +func (p *provider) doBlockAffis(pool *calicov3.IPPool, do func(calicoset.Interface, *calicov3.BlockAffinity) error) error { _, cidrNet, _ := cnet.ParseCIDR(pool.Spec.CIDR) - blockAffis, err := c.client.CrdCalicov3().BlockAffinities().List(context.TODO(), v1.ListOptions{}) + blockAffis, err := p.client.CrdCalicov3().BlockAffinities().List(context.TODO(), v1.ListOptions{}) if err != nil { return err } @@ -232,7 +232,7 @@ func (c provider) doBlockAffis(pool *calicov3.IPPool, do func(calicoset.Interfac continue } - err = do(c.client, &blockAffi) + err = do(p.client, &blockAffi) if err != nil { return err } @@ -241,34 +241,17 @@ func (c provider) doBlockAffis(pool *calicov3.IPPool, do func(calicoset.Interfac return nil } -func (c provider) listBlocks(pool *calicov3.IPPool) ([]calicov3.IPAMBlock, error) { - _, cidrNet, _ := cnet.ParseCIDR(pool.Spec.CIDR) - - blocks, err := c.client.CrdCalicov3().IPAMBlocks().List(context.TODO(), v1.ListOptions{}) - if err != nil { - return nil, err - } - - var result []calicov3.IPAMBlock - for _, block := range blocks.Items { - _, blockCIDR, _ := cnet.ParseCIDR(block.Spec.CIDR) - if !cidrNet.IsNetOverlap(blockCIDR.IPNet) { - continue - } - result = append(result, block) - } - - return result, nil -} - -func (c provider) doBlocks(pool *calicov3.IPPool, do func(calicoset.Interface, *calicov3.IPAMBlock) error) error { - blocks, err := c.listBlocks(pool) +func (p *provider) doBlocks(pool *calicov3.IPPool, do func(calicoset.Interface, *calicov3.IPAMBlock) error) error { + blocks, err := p.block.Lister().List(labels.SelectorFromSet( + labels.Set{ + v1alpha1.IPPoolNameLabel: pool.Name, + })) if err != nil { return err } for _, block := range blocks { - err = do(c.client, &block) + err = do(p.client, block) if err != nil { return err } @@ -280,8 +263,9 @@ func (c provider) doBlocks(pool *calicov3.IPPool, do func(calicoset.Interface, * func deleteBlock(c calicoset.Interface, block *calicov3.IPAMBlock) error { if block.Empty() { if !block.Spec.Deleted { - block.Spec.Deleted = true - _, err := c.CrdCalicov3().IPAMBlocks().Update(context.TODO(), block, v1.UpdateOptions{}) + clone := block.DeepCopy() + clone.Spec.Deleted = true + _, err := c.CrdCalicov3().IPAMBlocks().Update(context.TODO(), clone, v1.UpdateOptions{}) if err != nil { return err } @@ -297,7 +281,7 @@ func deleteBlock(c calicoset.Interface, block *calicov3.IPAMBlock) error { return nil } -func (c provider) DeleteIPPool(pool *v1alpha1.IPPool) (bool, error) { +func (p *provider) DeleteIPPool(pool *v1alpha1.IPPool) (bool, error) { // Deleting a pool requires a little care because of existing endpoints // using IP addresses allocated in the pool. We do the deletion in // the following steps: @@ -306,7 +290,7 @@ func (c provider) DeleteIPPool(pool *v1alpha1.IPPool) (bool, error) { // - delete the pool // Get the pool so that we can find the CIDR associated with it. - calicoPool, err := c.client.CrdCalicov3().IPPools().Get(context.TODO(), pool.Name, v1.GetOptions{}) + calicoPool, err := p.client.CrdCalicov3().IPPools().Get(context.TODO(), pool.Name, v1.GetOptions{}) if err != nil { if k8serrors.IsNotFound(err) { return true, nil @@ -318,14 +302,14 @@ func (c provider) DeleteIPPool(pool *v1alpha1.IPPool) (bool, error) { if !calicoPool.Spec.Disabled { calicoPool.Spec.Disabled = true - calicoPool, err = c.client.CrdCalicov3().IPPools().Update(context.TODO(), calicoPool, v1.UpdateOptions{}) + calicoPool, err = p.client.CrdCalicov3().IPPools().Update(context.TODO(), calicoPool, v1.UpdateOptions{}) if err != nil { return false, err } } //If the address pool is being used, we return, avoiding deletions that cause other problems. - stat, err := c.GetIPPoolStats(pool) + stat, err := p.GetIPPoolStats(pool) if err != nil { return false, err } @@ -334,13 +318,13 @@ func (c provider) DeleteIPPool(pool *v1alpha1.IPPool) (bool, error) { } //set blockaffi to pendingdelete - err = c.doBlockAffis(calicoPool, setBlockAffiDeletion) + err = p.doBlockAffis(calicoPool, setBlockAffiDeletion) if err != nil { return false, err } //delete block - err = c.doBlocks(calicoPool, deleteBlock) + err = p.doBlocks(calicoPool, deleteBlock) if err != nil { if errors.Is(err, ErrBlockInuse) { return false, nil @@ -349,13 +333,13 @@ func (c provider) DeleteIPPool(pool *v1alpha1.IPPool) (bool, error) { } //delete blockaffi - err = c.doBlockAffis(calicoPool, deleteBlockAffi) + err = p.doBlockAffis(calicoPool, deleteBlockAffi) if err != nil { return false, err } //delete calico ippool - err = c.client.CrdCalicov3().IPPools().Delete(context.TODO(), calicoPool.Name, v1.DeleteOptions{}) + err = p.client.CrdCalicov3().IPPools().Delete(context.TODO(), calicoPool.Name, v1.DeleteOptions{}) if err != nil { return false, err } @@ -365,14 +349,14 @@ func (c provider) DeleteIPPool(pool *v1alpha1.IPPool) (bool, error) { } //Synchronizing address pools at boot time -func (c provider) syncIPPools() error { - calicoPools, err := c.client.CrdCalicov3().IPPools().List(context.TODO(), v1.ListOptions{}) +func (p *provider) syncIPPools() error { + calicoPools, err := p.client.CrdCalicov3().IPPools().List(context.TODO(), v1.ListOptions{}) if err != nil { klog.V(4).Infof("syncIPPools: cannot list calico ippools, err=%v", err) return err } - pools, err := c.ksclient.NetworkV1alpha1().IPPools().List(context.TODO(), v1.ListOptions{}) + pools, err := p.ksclient.NetworkV1alpha1().IPPools().List(context.TODO(), v1.ListOptions{}) if err != nil { klog.V(4).Infof("syncIPPools: cannot list kubesphere ippools, err=%v", err) return err @@ -402,7 +386,7 @@ func (c provider) syncIPPools() error { Status: v1alpha1.IPPoolStatus{}, } - _, err = c.ksclient.NetworkV1alpha1().IPPools().Create(context.TODO(), pool, v1.CreateOptions{}) + _, err = p.ksclient.NetworkV1alpha1().IPPools().Create(context.TODO(), pool, v1.CreateOptions{}) if err != nil { klog.V(4).Infof("syncIPPools: cannot create kubesphere ippools, err=%v", err) return err @@ -413,7 +397,7 @@ func (c provider) syncIPPools() error { return nil } -func (p provider) getAssociatedWorkspaces(pool *v1alpha1.IPPool) ([]string, error) { +func (p *provider) getAssociatedWorkspaces(pool *v1alpha1.IPPool) ([]string, error) { var result []string poolLabel := constants.WorkspaceLabelKey @@ -430,10 +414,19 @@ func (p provider) getAssociatedWorkspaces(pool *v1alpha1.IPPool) ([]string, erro return result, nil } - return append(result, pool.GetLabels()[poolLabel]), nil + wk := pool.GetLabels()[poolLabel] + _, err := p.ksclient.TenantV1alpha1().Workspaces().Get(context.TODO(), wk, v1.GetOptions{}) + if k8serrors.IsNotFound(err) { + clone := pool.DeepCopy() + delete(clone.GetLabels(), poolLabel) + _, err := p.ksclient.NetworkV1alpha1().IPPools().Update(context.TODO(), clone, v1.UpdateOptions{}) + return nil, err + } + + return append(result, wk), err } -func (p provider) getWorkspaceStatus(name string, poolName string) (*v1alpha1.WorkspaceStatus, error) { +func (p *provider) getWorkspaceStatus(name string, poolName string) (*v1alpha1.WorkspaceStatus, error) { var result v1alpha1.WorkspaceStatus namespaces, err := p.k8sclient.CoreV1().Namespaces().List(context.TODO(), v1.ListOptions{ @@ -448,12 +441,19 @@ func (p provider) getWorkspaceStatus(name string, poolName string) (*v1alpha1.Wo } for _, ns := range namespaces.Items { - pods, err := p.k8sclient.CoreV1().Pods(ns.GetName()).List(context.TODO(), v1.ListOptions{}) + pods, err := p.k8sclient.CoreV1().Pods(ns.GetName()).List(context.TODO(), v1.ListOptions{ + LabelSelector: labels.SelectorFromSet( + labels.Set{ + v1alpha1.IPPoolNameLabel: poolName, + }, + ).String(), + }) if err != nil { return nil, err } + for _, pod := range pods.Items { - if pod.GetLabels() != nil && pod.GetLabels()[v1alpha1.IPPoolNameLabel] == poolName { + if pod.Status.Phase != corev1.PodSucceeded { result.Allocations++ } } @@ -462,11 +462,25 @@ func (p provider) getWorkspaceStatus(name string, poolName string) (*v1alpha1.Wo return &result, nil } -func (p provider) Type() string { +func (p *provider) Type() string { return v1alpha1.IPPoolTypeCalico } -func (p provider) SyncStatus(stopCh <-chan struct{}, q workqueue.RateLimitingInterface) error { +func (p *provider) UpdateNamespace(ns *corev1.Namespace, pools []string) error { + if pools != nil { + annostrs, _ := json.Marshal(pools) + if ns.Annotations == nil { + ns.Annotations = make(map[string]string) + } + ns.Annotations[CalicoAnnotationIPPoolV4] = string(annostrs) + } else { + delete(ns.Annotations, CalicoAnnotationIPPoolV4) + } + + return nil +} + +func (p *provider) SyncStatus(stopCh <-chan struct{}, q workqueue.RateLimitingInterface) error { defer utilruntime.HandleCrash() defer p.queue.ShutDown() @@ -488,7 +502,7 @@ func (p provider) SyncStatus(stopCh <-chan struct{}, q workqueue.RateLimitingInt return nil } -func (p provider) processBlock(name string) error { +func (p *provider) processBlock(name string) error { block, err := p.block.Lister().Get(name) if err != nil { if k8serrors.IsNotFound(err) { @@ -510,10 +524,11 @@ func (p provider) processBlock(name string) error { if poolCIDR.IsNetOverlap(blockCIDR.IPNet) { poolName = pool.Name - block.Labels = map[string]string{ + clone := block.DeepCopy() + clone.Labels = map[string]string{ v1alpha1.IPPoolNameLabel: pool.Name, } - p.client.CrdCalicov3().IPAMBlocks().Update(context.TODO(), block, v1.UpdateOptions{}) + p.client.CrdCalicov3().IPAMBlocks().Update(context.TODO(), clone, v1.UpdateOptions{}) break } } @@ -529,52 +544,35 @@ func (p provider) processBlock(name string) error { pod, err := p.pods.Lister().Pods(namespace).Get(name) if err != nil { - continue + if k8serrors.IsNotFound(err) { + continue + } + return err } - labels := pod.GetLabels() + clone := pod.DeepCopy() + labels := clone.GetLabels() if labels != nil { poolLabel := labels[v1alpha1.IPPoolNameLabel] if poolLabel != "" { continue } + } else { + clone.Labels = make(map[string]string) } - retry.RetryOnConflict(retry.DefaultBackoff, func() error { - pod, err = p.k8sclient.CoreV1().Pods(namespace).Get(context.TODO(), name, v1.GetOptions{}) - if err != nil { - return err - } - - labels := pod.GetLabels() - if labels != nil { - poolLabel := labels[v1alpha1.IPPoolNameLabel] - if poolLabel != "" { - return nil - } - } else { - pod.Labels = make(map[string]string) - } - - if pod.GetAnnotations() == nil { - pod.Annotations = make(map[string]string) - } - - annostrs, _ := json.Marshal([]string{poolName}) - pod.GetAnnotations()[CalicoAnnotationIPPoolV4] = string(annostrs) - pod.Labels[v1alpha1.IPPoolNameLabel] = poolName - - _, err = p.k8sclient.CoreV1().Pods(namespace).Update(context.TODO(), pod, v1.UpdateOptions{}) + clone.Labels[v1alpha1.IPPoolNameLabel] = poolName + _, err = p.k8sclient.CoreV1().Pods(namespace).Update(context.TODO(), clone, v1.UpdateOptions{}) + if err != nil { return err - }) + } } - p.poolQueue.Add(poolName) return nil } -func (p provider) processBlockItem() bool { +func (p *provider) processBlockItem() bool { key, quit := p.queue.Get() if quit { return false @@ -592,12 +590,12 @@ func (p provider) processBlockItem() bool { return true } -func (p provider) runWorker() { +func (p *provider) runWorker() { for p.processBlockItem() { } } -func (p provider) addBlock(obj interface{}) { +func (p *provider) addBlock(obj interface{}) { block, ok := obj.(*calicov3.IPAMBlock) if !ok { return @@ -606,7 +604,7 @@ func (p provider) addBlock(obj interface{}) { p.queue.Add(block.Name) } -func (p provider) Default(obj runtime.Object) error { +func (p *provider) Default(obj runtime.Object) error { pod, ok := obj.(*corev1.Pod) if !ok { return nil @@ -639,7 +637,18 @@ func (p provider) Default(obj runtime.Object) error { return nil } -func NewProvider(podInformer informercorev1.PodInformer, ksclient kubesphereclient.Interface, k8sClient clientset.Interface, k8sOptions *k8s.KubernetesOptions) provider { +func (p *provider) addPod(obj interface{}) { + pod, _ := obj.(*corev1.Pod) + + if pod.Labels != nil { + pool := pod.Labels[v1alpha1.IPPoolNameLabel] + if pool != "" && p.poolQueue != nil { + p.poolQueue.Add(pool) + } + } +} + +func NewProvider(k8sInformer k8sinformers.SharedInformerFactory, ksclient kubesphereclient.Interface, k8sClient clientset.Interface, k8sOptions *k8s.KubernetesOptions) *provider { config, err := clientcmd.BuildConfigFromFlags("", k8sOptions.KubeConfig) if err != nil { klog.Fatalf("failed to build k8s config , err=%v", err) @@ -677,14 +686,27 @@ func NewProvider(podInformer informercorev1.PodInformer, ksclient kubesphereclie } } - p := provider{ + p := &provider{ client: client, ksclient: ksclient, k8sclient: k8sClient, - pods: podInformer, queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "calicoBlock"), options: opts, } + p.pods = k8sInformer.Core().V1().Pods() + + p.pods.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + UpdateFunc: func(old, new interface{}) { + poolOld := old.(*corev1.Pod).Labels[v1alpha1.IPPoolNameLabel] + poolNew := new.(*corev1.Pod).Labels[v1alpha1.IPPoolNameLabel] + if poolNew == poolOld { + return + } + p.addPod(new) + }, + DeleteFunc: p.addPod, + AddFunc: p.addPod, + }) blockI := calicoInformer.NewSharedInformerFactory(client, defaultResync).Crd().Calicov3().IPAMBlocks() blockI.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ diff --git a/pkg/simple/client/network/ippool/ipam/ipam.go b/pkg/simple/client/network/ippool/ipam/ipam.go index a4df002bf..646d03e5a 100644 --- a/pkg/simple/client/network/ippool/ipam/ipam.go +++ b/pkg/simple/client/network/ippool/ipam/ipam.go @@ -423,6 +423,10 @@ func (c IPAMClient) GetUtilization(args GetUtilizationArgs) ([]*PoolUtilization, return nil, err } + if len(allPools) <= 0 { + return nil, fmt.Errorf("not found pool") + } + // Identify the ones we want and create a PoolUtilization for each of those. wantAllPools := len(args.Pools) == 0 wantedPools := set.FromArray(args.Pools) diff --git a/pkg/simple/client/network/ippool/provider.go b/pkg/simple/client/network/ippool/provider.go index 20e60b5f6..43eae33fe 100644 --- a/pkg/simple/client/network/ippool/provider.go +++ b/pkg/simple/client/network/ippool/provider.go @@ -17,8 +17,9 @@ limitations under the License. package ippool import ( + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" - v1 "k8s.io/client-go/informers/core/v1" + k8sinformers "k8s.io/client-go/informers" clientset "k8s.io/client-go/kubernetes" "k8s.io/client-go/util/workqueue" networkv1alpha1 "kubesphere.io/kubesphere/pkg/apis/network/v1alpha1" @@ -35,6 +36,7 @@ type Provider interface { UpdateIPPool(pool *networkv1alpha1.IPPool) error GetIPPoolStats(pool *networkv1alpha1.IPPool) (*networkv1alpha1.IPPool, error) SyncStatus(stopCh <-chan struct{}, q workqueue.RateLimitingInterface) error + UpdateNamespace(ns *corev1.Namespace, pools []string) error Type() string Default(obj runtime.Object) error } @@ -52,6 +54,10 @@ func (p provider) Default(obj runtime.Object) error { return nil } +func (p provider) UpdateNamespace(ns *corev1.Namespace, pools []string) error { + return nil +} + func (p provider) DeleteIPPool(pool *networkv1alpha1.IPPool) (bool, error) { blocks, err := p.ipamclient.ListBlocks(pool.Name) if err != nil { @@ -110,7 +116,7 @@ func newProvider(clientset kubesphereclient.Interface) provider { } } -func NewProvider(podInformer v1.PodInformer, clientset kubesphereclient.Interface, client clientset.Interface, pt string, k8sOptions *k8s.KubernetesOptions) Provider { +func NewProvider(k8sInformer k8sinformers.SharedInformerFactory, clientset kubesphereclient.Interface, client clientset.Interface, pt string, k8sOptions *k8s.KubernetesOptions) Provider { var p Provider switch pt { @@ -120,7 +126,7 @@ func NewProvider(podInformer v1.PodInformer, clientset kubesphereclient.Interfac ipamclient: ipam.NewIPAMClient(clientset, networkv1alpha1.VLAN), } case networkv1alpha1.IPPoolTypeCalico: - p = calicoclient.NewProvider(podInformer, clientset, client, k8sOptions) + p = calicoclient.NewProvider(k8sInformer, clientset, client, k8sOptions) } return p