├── config ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── manager │ ├── kustomization.yaml │ ├── service.yaml │ ├── manager.yaml │ └── config.yaml ├── certmanager │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── certificate.yaml ├── webhook │ ├── kustomization.yaml │ ├── namespace_selector_patch.yaml │ ├── service.yaml │ ├── manifests.yaml │ └── kustomizeconfig.yaml ├── default │ ├── manager_config_patch.yaml │ ├── manager_webhook_patch.yaml │ ├── webhookcainjection_patch.yaml │ ├── manager_auth_proxy_patch.yaml │ └── kustomization.yaml ├── samples │ ├── autoscaling_v1alpha1_intelligenthorizontalpodautoscaler.yaml │ ├── autoscaling_v1alpha1_replicaprofile.yaml │ └── autoscaling_v1alpha1_horizontalportrait.yaml ├── crd │ ├── patches │ │ ├── cainjection_in_replicaprofiles.yaml │ │ ├── cainjection_in_horizontalportraits.yaml │ │ ├── cainjection_in_intelligenthorizontalpodautoscalers.yaml │ │ ├── webhook_in_replicaprofiles.yaml │ │ ├── webhook_in_horizontalportraits.yaml │ │ └── webhook_in_intelligenthorizontalpodautoscalers.yaml │ ├── kustomizeconfig.yaml │ └── kustomization.yaml └── rbac │ ├── auth_proxy_client_clusterrole.yaml │ ├── intelligenthorizontalpodautoscaler_viewer_role.yaml │ ├── role_binding.yaml │ ├── auth_proxy_role_binding.yaml │ ├── intelligenthorizontalpodautoscaler_editor_role.yaml │ ├── leader_election_role_binding.yaml │ ├── algorithm_job_role_binding.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_service.yaml │ ├── replicaprofile_viewer_role.yaml │ ├── horizontalportrait_viewer_role.yaml │ ├── service_account.yaml │ ├── kustomization.yaml │ ├── replicaprofile_editor_role.yaml │ ├── horizontalportrait_editor_role.yaml │ ├── algorithm_job_role.yaml │ ├── leader_election_role.yaml │ └── role.yaml ├── logo ├── logo-with-black-text.png ├── logo-with-white-text.png └── logo.svg ├── .dockerignore ├── CONTRIBUTING.md ├── algorithm ├── kapacity │ ├── metric │ │ ├── pb │ │ │ ├── metric_pb2_grpc.py │ │ │ ├── __init__.py │ │ │ ├── provider.proto │ │ │ ├── provider_pb2.pyi │ │ │ ├── provider_pb2.py │ │ │ ├── metric.proto │ │ │ └── provider_pb2_grpc.py │ │ └── __init__.py │ ├── __init__.py │ ├── portrait │ │ ├── __init__.py │ │ └── horizontal │ │ │ ├── __init__.py │ │ │ └── predictive │ │ │ └── __init__.py │ └── timeseries │ │ ├── __init__.py │ │ └── forecasting │ │ ├── __init__.py │ │ └── train.py ├── environment.yml ├── horizontal-predictive.Dockerfile └── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature-request.yaml │ └── bug-report.yaml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── release.yaml │ ├── release-algorithm.yaml │ └── ci.yaml ├── .gitignore ├── hack └── boilerplate.go.txt ├── pkg ├── pod │ ├── labels.go │ ├── traffic │ │ ├── interfaces.go │ │ ├── readinessgate.go │ │ └── readinessgate_test.go │ └── sorter │ │ └── interfaces.go ├── metric │ ├── labels.go │ ├── provider │ │ ├── interfaces.go │ │ └── prometheus │ │ │ ├── resource.go │ │ │ ├── external_series_registry.go │ │ │ ├── metrics_config.go │ │ │ └── object_series_registry.go │ ├── value.go │ ├── service │ │ └── api │ │ │ ├── provider.proto │ │ │ └── metric.proto │ └── query.go ├── util │ ├── math.go │ ├── time_test.go │ ├── math_test.go │ ├── time.go │ ├── common.go │ ├── wait.go │ ├── common_test.go │ ├── client_test.go │ ├── pod.go │ ├── client.go │ ├── apimachinery.go │ ├── pod_test.go │ ├── apimachinery_test.go │ └── wait_test.go ├── portrait │ ├── algorithm │ │ └── externaljob │ │ │ ├── resultfetcher │ │ │ ├── interfaces.go │ │ │ └── configmap_test.go │ │ │ └── jobcontroller │ │ │ ├── interfaces.go │ │ │ └── cronjob_test.go │ ├── generator │ │ ├── interfaces.go │ │ └── reactive │ │ │ └── metrics_client_test.go │ └── provider │ │ ├── interfaces.go │ │ ├── static.go │ │ ├── static_test.go │ │ ├── cron.go │ │ └── cron_test.go ├── workload │ ├── interfaces.go │ ├── deployment.go │ ├── statefulset.go │ ├── deployment_test.go │ ├── replicaset_test.go │ ├── statefulset_test.go │ └── replicaset.go └── scale │ └── scaler.go ├── controllers ├── common.go └── autoscaling │ └── suite_test.go ├── .golangci.yaml ├── PROJECT ├── internal ├── webhook │ ├── server.go │ ├── common │ │ └── types.go │ └── pod │ │ └── mutating │ │ ├── readinessgate.go │ │ └── handler.go └── grpc │ └── server.go ├── Dockerfile ├── apis └── autoscaling │ └── v1alpha1 │ └── groupversion_info.go └── README_zh.md /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - config.yaml 3 | - manager.yaml 4 | - service.yaml 5 | -------------------------------------------------------------------------------- /logo/logo-with-black-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traas-stack/kapacity/HEAD/logo/logo-with-black-text.png -------------------------------------------------------------------------------- /logo/logo-with-white-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traas-stack/kapacity/HEAD/logo/logo-with-white-text.png -------------------------------------------------------------------------------- /config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Please visit the [Contributing page](https://kapacity.netlify.app/docs/contribution-guidelines/) on our website for up-to-date details. 4 | -------------------------------------------------------------------------------- /config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | 8 | patchesStrategicMerge: 9 | - namespace_selector_patch.yaml 10 | -------------------------------------------------------------------------------- /algorithm/kapacity/metric/pb/metric_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | """Client and server classes corresponding to protobuf-defined services.""" 3 | import grpc 4 | 5 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Support Questions 4 | url: https://github.com/traas-stack/kapacity/discussions/new/choose 5 | about: Need support or have some general questions? You can ask in GitHub Discussions. 6 | -------------------------------------------------------------------------------- /config/samples/autoscaling_v1alpha1_intelligenthorizontalpodautoscaler.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling.kapacitystack.io/v1alpha1 2 | kind: IntelligentHorizontalPodAutoscaler 3 | metadata: 4 | name: intelligenthorizontalpodautoscaler-sample 5 | spec: 6 | # TODO(user): Add fields here 7 | -------------------------------------------------------------------------------- /algorithm/environment.yml: -------------------------------------------------------------------------------- 1 | name: kapacity 2 | dependencies: 3 | - python=3.11.* 4 | - pytorch::pytorch=2.0.* 5 | - pandas=1.5.* 6 | - pyyaml=6.0.* 7 | - scikit-learn=1.2.* 8 | - lightgbm=3.3.* 9 | - protobuf=3.20.* 10 | - grpcio=1.48.* 11 | - python-kubernetes=23.6.* 12 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_replicaprofiles.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: replicaprofiles.autoscaling.kapacitystack.io 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_horizontalportraits.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: horizontalportraits.autoscaling.kapacitystack.io 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_intelligenthorizontalpodautoscalers.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: intelligenthorizontalpodautoscalers.autoscaling.kapacitystack.io 8 | -------------------------------------------------------------------------------- /algorithm/horizontal-predictive.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM continuumio/miniconda3:23.5.2-0 2 | 3 | WORKDIR /algorithm 4 | 5 | ENV PYTHONPATH=/algorithm 6 | 7 | COPY environment.yml . 8 | RUN conda env create -f environment.yml 9 | 10 | COPY kapacity/ kapacity/ 11 | 12 | ENTRYPOINT ["conda", "run", "--no-capture-output", "-n", "kapacity", \ 13 | "python", "/algorithm/kapacity/portrait/horizontal/predictive/main.py"] 14 | -------------------------------------------------------------------------------- /config/webhook/namespace_selector_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: MutatingWebhookConfiguration 3 | metadata: 4 | name: mutating-webhook-configuration 5 | webhooks: 6 | # exclude the namespace where the webhook running to avoid deadlock on pod creating 7 | - name: mpod.kb.io 8 | namespaceSelector: 9 | matchExpressions: 10 | - key: kubernetes.io/metadata.name 11 | operator: NotIn 12 | values: 13 | - kapacity-system 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Provide supporting details for a feature that wanted. 3 | labels: kind/feature 4 | body: 5 | - type: textarea 6 | id: feature 7 | attributes: 8 | label: What would you like to be added? 9 | validations: 10 | required: true 11 | 12 | - type: textarea 13 | id: rationale 14 | attributes: 15 | label: Why is this needed? 16 | validations: 17 | required: true -------------------------------------------------------------------------------- /config/samples/autoscaling_v1alpha1_replicaprofile.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling.kapacitystack.io/v1alpha1 2 | kind: ReplicaProfile 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: replicaprofile 6 | app.kubernetes.io/instance: replicaprofile-sample 7 | app.kubernetes.io/part-of: kapacity 8 | app.kuberentes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: kapacity 10 | name: replicaprofile-sample 11 | spec: 12 | # TODO(user): Add fields here 13 | -------------------------------------------------------------------------------- /config/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref and var substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: cert-manager.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: cert-manager.io 8 | path: spec/issuerRef/name 9 | 10 | varReference: 11 | - kind: Certificate 12 | group: cert-manager.io 13 | path: spec/commonName 14 | - kind: Certificate 15 | group: cert-manager.io 16 | path: spec/dnsNames 17 | -------------------------------------------------------------------------------- /config/samples/autoscaling_v1alpha1_horizontalportrait.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling.kapacitystack.io/v1alpha1 2 | kind: HorizontalPortrait 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: horizontalportrait 6 | app.kubernetes.io/instance: horizontalportrait-sample 7 | app.kubernetes.io/part-of: kapacity 8 | app.kuberentes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: kapacity 10 | name: horizontalportrait-sample 11 | spec: 12 | # TODO(user): Add fields here 13 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_replicaprofiles.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: replicaprofiles.autoscaling.kapacitystack.io 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_horizontalportraits.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: horizontalportraits.autoscaling.kapacitystack.io 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: metrics-reader 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: kapacity 9 | app.kubernetes.io/part-of: kapacity 10 | app.kubernetes.io/managed-by: kustomize 11 | name: metrics-reader 12 | rules: 13 | - nonResourceURLs: 14 | - "/metrics" 15 | verbs: 16 | - get 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | bin 8 | testbin/* 9 | Dockerfile.cross 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Kubernetes Generated files - skip generated files, except for vendored files 18 | 19 | !vendor/**/zz_generated.* 20 | 21 | # editor and IDE paraphernalia 22 | .idea 23 | *.swp 24 | *.swo 25 | *~ 26 | 27 | # macOS paraphernalia 28 | .DS_Store 29 | -------------------------------------------------------------------------------- /config/manager/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: service 6 | app.kubernetes.io/instance: grpc-service 7 | app.kubernetes.io/component: manager 8 | app.kubernetes.io/created-by: kapacity 9 | app.kubernetes.io/part-of: kapacity 10 | app.kubernetes.io/managed-by: kustomize 11 | name: grpc-service 12 | namespace: system 13 | spec: 14 | ports: 15 | - port: 9090 16 | protocol: TCP 17 | targetPort: 9090 18 | selector: 19 | control-plane: controller-manager 20 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_intelligenthorizontalpodautoscalers.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: intelligenthorizontalpodautoscalers.autoscaling.kapacitystack.io 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: service 6 | app.kubernetes.io/instance: webhook-service 7 | app.kubernetes.io/component: webhook 8 | app.kubernetes.io/created-by: project-v3 9 | app.kubernetes.io/part-of: project-v3 10 | app.kubernetes.io/managed-by: kustomize 11 | name: webhook-service 12 | namespace: system 13 | spec: 14 | ports: 15 | - port: 443 16 | protocol: TCP 17 | targetPort: 9443 18 | selector: 19 | control-plane: controller-manager 20 | -------------------------------------------------------------------------------- /config/rbac/intelligenthorizontalpodautoscaler_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view intelligenthorizontalpodautoscalers. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: intelligenthorizontalpodautoscaler-viewer-role 6 | rules: 7 | - apiGroups: 8 | - autoscaling.kapacitystack.io 9 | resources: 10 | - intelligenthorizontalpodautoscalers 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - autoscaling.kapacitystack.io 17 | resources: 18 | - intelligenthorizontalpodautoscalers/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: manager-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: kapacity 9 | app.kubernetes.io/part-of: kapacity 10 | app.kubernetes.io/managed-by: kustomize 11 | name: manager-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: manager-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright YEAR The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /config/default/manager_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | ports: 12 | - containerPort: 9443 13 | name: webhook-server 14 | protocol: TCP 15 | volumeMounts: 16 | - mountPath: /tmp/k8s-webhook-server/serving-certs 17 | name: webhook-cert 18 | readOnly: true 19 | volumes: 20 | - name: webhook-cert 21 | secret: 22 | defaultMode: 420 23 | secretName: webhook-server-cert 24 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: proxy-rolebinding 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: kapacity 9 | app.kubernetes.io/part-of: kapacity 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: proxy-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /algorithm/kapacity/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 The Kapacity Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /config/rbac/intelligenthorizontalpodautoscaler_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit intelligenthorizontalpodautoscalers. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: intelligenthorizontalpodautoscaler-editor-role 6 | rules: 7 | - apiGroups: 8 | - autoscaling.kapacitystack.io 9 | resources: 10 | - intelligenthorizontalpodautoscalers 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - autoscaling.kapacitystack.io 21 | resources: 22 | - intelligenthorizontalpodautoscalers/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: rolebinding 6 | app.kubernetes.io/instance: leader-election-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: kapacity 9 | app.kubernetes.io/part-of: kapacity 10 | app.kubernetes.io/managed-by: kustomize 11 | name: leader-election-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: Role 15 | name: leader-election-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /algorithm/kapacity/metric/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 The Kapacity Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /config/rbac/algorithm_job_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: algorithm-job-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: kapacity 9 | app.kubernetes.io/part-of: kapacity 10 | app.kubernetes.io/managed-by: kustomize 11 | name: algorithm-job-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: algorithm-job-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: algorithm-job 19 | namespace: system 20 | -------------------------------------------------------------------------------- /algorithm/kapacity/metric/pb/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 The Kapacity Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /algorithm/kapacity/portrait/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 The Kapacity Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /algorithm/kapacity/timeseries/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 The Kapacity Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: proxy-role 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: kapacity 9 | app.kubernetes.io/part-of: kapacity 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-role 12 | rules: 13 | - apiGroups: 14 | - authentication.k8s.io 15 | resources: 16 | - tokenreviews 17 | verbs: 18 | - create 19 | - apiGroups: 20 | - authorization.k8s.io 21 | resources: 22 | - subjectaccessreviews 23 | verbs: 24 | - create 25 | -------------------------------------------------------------------------------- /config/webhook/manifests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: admissionregistration.k8s.io/v1 3 | kind: MutatingWebhookConfiguration 4 | metadata: 5 | creationTimestamp: null 6 | name: mutating-webhook-configuration 7 | webhooks: 8 | - admissionReviewVersions: 9 | - v1 10 | - v1beta1 11 | clientConfig: 12 | service: 13 | name: webhook-service 14 | namespace: system 15 | path: /mutate-v1-pod 16 | failurePolicy: Fail 17 | name: mpod.kb.io 18 | rules: 19 | - apiGroups: 20 | - "" 21 | apiVersions: 22 | - v1 23 | operations: 24 | - CREATE 25 | - UPDATE 26 | resources: 27 | - pods 28 | - pods/status 29 | sideEffects: None 30 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: service 7 | app.kubernetes.io/instance: controller-manager-metrics-service 8 | app.kubernetes.io/component: kube-rbac-proxy 9 | app.kubernetes.io/created-by: kapacity 10 | app.kubernetes.io/part-of: kapacity 11 | app.kubernetes.io/managed-by: kustomize 12 | name: controller-manager-metrics-service 13 | namespace: system 14 | spec: 15 | ports: 16 | - name: https 17 | port: 8443 18 | protocol: TCP 19 | targetPort: https 20 | selector: 21 | control-plane: controller-manager 22 | -------------------------------------------------------------------------------- /algorithm/kapacity/portrait/horizontal/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 The Kapacity Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /algorithm/kapacity/timeseries/forecasting/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 The Kapacity Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /algorithm/kapacity/portrait/horizontal/predictive/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 The Kapacity Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /pkg/pod/labels.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package pod 18 | 19 | const ( 20 | LabelState = "kapacitystack.io/pod-state" 21 | ) 22 | -------------------------------------------------------------------------------- /controllers/common.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | const ( 20 | Finalizer = "kapacitystack.io/finalizer" 21 | ) 22 | -------------------------------------------------------------------------------- /pkg/metric/labels.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package metric 18 | 19 | const ( 20 | LabelNamespace = "namespace" 21 | LabelPodName = "pod" 22 | ) 23 | -------------------------------------------------------------------------------- /config/rbac/replicaprofile_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view replicaprofiles. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: replicaprofile-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: kapacity 10 | app.kubernetes.io/part-of: kapacity 11 | app.kubernetes.io/managed-by: kustomize 12 | name: replicaprofile-viewer-role 13 | rules: 14 | - apiGroups: 15 | - autoscaling.kapacitystack.io 16 | resources: 17 | - replicaprofiles 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - autoscaling.kapacitystack.io 24 | resources: 25 | - replicaprofiles/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/horizontalportrait_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view horizontalportraits. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: horizontalportrait-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: kapacity 10 | app.kubernetes.io/part-of: kapacity 11 | app.kubernetes.io/managed-by: kustomize 12 | name: horizontalportrait-viewer-role 13 | rules: 14 | - apiGroups: 15 | - autoscaling.kapacitystack.io 16 | resources: 17 | - horizontalportraits 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - autoscaling.kapacitystack.io 24 | resources: 25 | - horizontalportraits/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: serviceaccount 6 | app.kuberentes.io/instance: controller-manager 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: kapacity 9 | app.kubernetes.io/part-of: kapacity 10 | app.kubernetes.io/managed-by: kustomize 11 | name: controller-manager 12 | namespace: system 13 | 14 | --- 15 | apiVersion: v1 16 | kind: ServiceAccount 17 | metadata: 18 | labels: 19 | app.kubernetes.io/name: serviceaccount 20 | app.kuberentes.io/instance: algorithm-job 21 | app.kubernetes.io/component: rbac 22 | app.kubernetes.io/created-by: kapacity 23 | app.kubernetes.io/part-of: kapacity 24 | app.kubernetes.io/managed-by: kustomize 25 | name: algorithm-job 26 | namespace: system 27 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | - algorithm_job_role.yaml 13 | - algorithm_job_role_binding.yaml 14 | # Comment the following 4 lines if you want to disable 15 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 16 | # which protects your /metrics endpoint. 17 | - auth_proxy_service.yaml 18 | - auth_proxy_role.yaml 19 | - auth_proxy_role_binding.yaml 20 | - auth_proxy_client_clusterrole.yaml 21 | -------------------------------------------------------------------------------- /config/rbac/replicaprofile_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit replicaprofiles. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: replicaprofile-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: kapacity 10 | app.kubernetes.io/part-of: kapacity 11 | app.kubernetes.io/managed-by: kustomize 12 | name: replicaprofile-editor-role 13 | rules: 14 | - apiGroups: 15 | - autoscaling.kapacitystack.io 16 | resources: 17 | - replicaprofiles 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - autoscaling.kapacitystack.io 28 | resources: 29 | - replicaprofiles/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | # Timeout for analysis, e.g. 30s, 5m. 3 | # Default: 1m 4 | timeout: 10m 5 | 6 | linters: 7 | # Disable all linters. 8 | # Default: false 9 | disable-all: true 10 | # Enable specific linter 11 | # https://golangci-lint.run/usage/linters/ 12 | enable: 13 | - errcheck 14 | - gofmt 15 | - goimports 16 | - gosimple 17 | - govet 18 | - importas 19 | - ineffassign 20 | - loggercheck 21 | - misspell 22 | - staticcheck 23 | - stylecheck 24 | - typecheck 25 | - unconvert 26 | - unused 27 | - usestdlibvars 28 | 29 | linters-settings: 30 | goimports: 31 | # A comma-separated list of prefixes, which, if set, checks import paths 32 | # with the given prefixes are grouped after 3rd-party packages. 33 | # Default: "" 34 | local-prefixes: github.com/traas-stack/kapacity 35 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | #### What type of PR is this? 8 | 23 | 24 | #### What this PR does / why we need it: 25 | 26 | #### Which issue(s) this PR fixes: 27 | 31 | Fixes # 32 | 33 | #### Special notes for your reviewer: 34 | -------------------------------------------------------------------------------- /config/rbac/horizontalportrait_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit horizontalportraits. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: horizontalportrait-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: kapacity 10 | app.kubernetes.io/part-of: kapacity 11 | app.kubernetes.io/managed-by: kustomize 12 | name: horizontalportrait-editor-role 13 | rules: 14 | - apiGroups: 15 | - autoscaling.kapacitystack.io 16 | resources: 17 | - horizontalportraits 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - autoscaling.kapacitystack.io 28 | resources: 29 | - horizontalportraits/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | app.kubernetes.io/name: servicemonitor 9 | app.kubernetes.io/instance: controller-manager-metrics-monitor 10 | app.kubernetes.io/component: metrics 11 | app.kubernetes.io/created-by: kapacity 12 | app.kubernetes.io/part-of: kapacity 13 | app.kubernetes.io/managed-by: kustomize 14 | name: controller-manager-metrics-monitor 15 | namespace: system 16 | spec: 17 | endpoints: 18 | - path: /metrics 19 | port: https 20 | scheme: https 21 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 22 | tlsConfig: 23 | insecureSkipVerify: true 24 | selector: 25 | matchLabels: 26 | control-plane: controller-manager 27 | -------------------------------------------------------------------------------- /config/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # the following config is for teaching kustomize where to look at when substituting vars. 2 | # It requires kustomize v2.1.0 or newer to work properly. 3 | nameReference: 4 | - kind: Service 5 | version: v1 6 | fieldSpecs: 7 | - kind: MutatingWebhookConfiguration 8 | group: admissionregistration.k8s.io 9 | path: webhooks/clientConfig/service/name 10 | - kind: ValidatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | 14 | namespace: 15 | - kind: MutatingWebhookConfiguration 16 | group: admissionregistration.k8s.io 17 | path: webhooks/clientConfig/service/namespace 18 | create: true 19 | - kind: ValidatingWebhookConfiguration 20 | group: admissionregistration.k8s.io 21 | path: webhooks/clientConfig/service/namespace 22 | create: true 23 | 24 | varReference: 25 | - path: metadata/annotations 26 | -------------------------------------------------------------------------------- /config/rbac/algorithm_job_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: algorithm-job-role 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: kapacity 9 | app.kubernetes.io/part-of: kapacity 10 | app.kubernetes.io/managed-by: kustomize 11 | name: algorithm-job-role 12 | rules: 13 | - apiGroups: 14 | - "" 15 | resources: 16 | - configmaps 17 | verbs: 18 | - create 19 | - get 20 | - list 21 | - update 22 | - watch 23 | - apiGroups: 24 | - "" 25 | resources: 26 | - pods 27 | verbs: 28 | - get 29 | - list 30 | - watch 31 | - apiGroups: 32 | - '*' 33 | resources: 34 | - '*/scale' 35 | verbs: 36 | - get 37 | - apiGroups: 38 | - autoscaling.kapacitystack.io 39 | resources: 40 | - horizontalportraits 41 | verbs: 42 | - get 43 | - list 44 | - watch 45 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: kapacitystack.io 2 | layout: 3 | - go.kubebuilder.io/v3 4 | multigroup: true 5 | projectName: kapacity 6 | repo: github.com/traas-stack/kapacity 7 | resources: 8 | - api: 9 | crdVersion: v1 10 | namespaced: true 11 | controller: true 12 | domain: kapacitystack.io 13 | group: autoscaling 14 | kind: ReplicaProfile 15 | path: github.com/traas-stack/kapacity/api/v1alpha1 16 | version: v1alpha1 17 | - api: 18 | crdVersion: v1 19 | namespaced: true 20 | controller: true 21 | domain: kapacitystack.io 22 | group: autoscaling 23 | kind: IntelligentHorizontalPodAutoscaler 24 | path: github.com/traas-stack/kapacity/api/v1alpha1 25 | version: v1alpha1 26 | - api: 27 | crdVersion: v1 28 | namespaced: true 29 | controller: true 30 | domain: kapacitystack.io 31 | group: autoscaling 32 | kind: HorizontalPortrait 33 | path: github.com/traas-stack/kapacity/api/v1alpha1 34 | version: v1alpha1 35 | version: "3" 36 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: role 7 | app.kubernetes.io/instance: leader-election-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: kapacity 10 | app.kubernetes.io/part-of: kapacity 11 | app.kubernetes.io/managed-by: kustomize 12 | name: leader-election-role 13 | rules: 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - configmaps 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - create 23 | - update 24 | - patch 25 | - delete 26 | - apiGroups: 27 | - coordination.k8s.io 28 | resources: 29 | - leases 30 | verbs: 31 | - get 32 | - list 33 | - watch 34 | - create 35 | - update 36 | - patch 37 | - delete 38 | - apiGroups: 39 | - "" 40 | resources: 41 | - events 42 | verbs: 43 | - create 44 | - patch 45 | -------------------------------------------------------------------------------- /pkg/pod/traffic/interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package traffic 18 | 19 | import ( 20 | "context" 21 | 22 | corev1 "k8s.io/api/core/v1" 23 | ) 24 | 25 | // Controller provide methods to control pod traffic. 26 | type Controller interface { 27 | // On turns on the specified pods' traffic. 28 | On(context.Context, []*corev1.Pod) error 29 | // Off turns off the specified pods' traffic. 30 | Off(context.Context, []*corev1.Pod) error 31 | } 32 | -------------------------------------------------------------------------------- /pkg/pod/sorter/interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package sorter 18 | 19 | import ( 20 | "context" 21 | 22 | corev1 "k8s.io/api/core/v1" 23 | ) 24 | 25 | // Interface provides a method to sort pods. 26 | // Check method comment for ordering requirement. 27 | type Interface interface { 28 | // Sort pods in descending "scale down" priority order, which means 29 | // the higher priority to be scaled down the pod is, the smaller will its index be. 30 | Sort(context.Context, []*corev1.Pod) ([]*corev1.Pod, error) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/util/math.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | // MaxInt32 returns the biggest one between two int32 numbers. 20 | func MaxInt32(a, b int32) int32 { 21 | if a >= b { 22 | return a 23 | } 24 | return b 25 | } 26 | 27 | // MinInt32 returns the smallest one between two int32 numbers. 28 | func MinInt32(a, b int32) int32 { 29 | if a <= b { 30 | return a 31 | } 32 | return b 33 | } 34 | 35 | // AbsInt32 returns the absolute value of an int32 number. 36 | func AbsInt32(x int32) int32 { 37 | if x < 0 { 38 | return -x 39 | } 40 | return x 41 | } 42 | -------------------------------------------------------------------------------- /pkg/metric/provider/interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package provider 18 | 19 | import ( 20 | "context" 21 | "time" 22 | 23 | "github.com/traas-stack/kapacity/pkg/metric" 24 | ) 25 | 26 | // Interface provide methods to query metrics. 27 | type Interface interface { 28 | // QueryLatest metrics. 29 | QueryLatest(ctx context.Context, query *metric.Query) ([]*metric.Sample, error) 30 | // Query historical metrics with arbitrary range and step. 31 | Query(ctx context.Context, query *metric.Query, start, end time.Time, step time.Duration) ([]*metric.Series, error) 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | permissions: 7 | contents: read 8 | packages: write 9 | jobs: 10 | build-and-push-image: 11 | name: Build and push image 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - name: Get tag 19 | run: echo "tag=$(git describe --tags --match 'v*')" >> $GITHUB_ENV 20 | - name: Set up QEMU 21 | uses: docker/setup-qemu-action@v3 22 | - name: Set up Docker Buildx 23 | uses: docker/setup-buildx-action@v3 24 | - name: Login to GitHub Container Registry 25 | uses: docker/login-action@v3 26 | with: 27 | registry: ghcr.io 28 | username: ${{ github.actor }} 29 | password: ${{ secrets.GITHUB_TOKEN }} 30 | - name: Build and push image 31 | uses: docker/build-push-action@v6 32 | with: 33 | context: . 34 | platforms: linux/amd64,linux/arm64 35 | push: true 36 | tags: ghcr.io/${{ github.repository_owner }}/kapacity-manager:${{ env.tag }} 37 | -------------------------------------------------------------------------------- /internal/webhook/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package webhook 18 | 19 | import ( 20 | "fmt" 21 | 22 | ctrl "sigs.k8s.io/controller-runtime" 23 | 24 | podmutating "github.com/traas-stack/kapacity/internal/webhook/pod/mutating" 25 | ) 26 | 27 | // SetupWithManager sets up the webhooks with the Manager. 28 | func SetupWithManager(mgr ctrl.Manager) error { 29 | srv := mgr.GetWebhookServer() 30 | if err := podmutating.HandlerRegister.RegisterToServerWithManager(srv, mgr); err != nil { 31 | return fmt.Errorf("failed to register pod mutating webhook: %v", err) 32 | } 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /pkg/util/time_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "testing" 21 | "time" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestIsCronActive(t *testing.T) { 27 | now := time.Now() 28 | 29 | // active 30 | startCron := "0 0 * * ?" 31 | endCron := "* * * * ?" 32 | isActive, _, err := IsCronActive(now, startCron, endCron) 33 | assert.Nil(t, err) 34 | assert.True(t, isActive) 35 | 36 | // inactive 37 | endCron = "0 0 * * ?" 38 | isActive, _, err = IsCronActive(now, startCron, endCron) 39 | assert.Nil(t, err) 40 | assert.False(t, isActive) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/metric/value.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package metric 18 | 19 | import ( 20 | "time" 21 | 22 | prommodel "github.com/prometheus/common/model" 23 | ) 24 | 25 | // Series is a stream of data points belonging to a metric. 26 | type Series struct { 27 | Points []Point 28 | Labels prommodel.LabelSet 29 | Window *time.Duration 30 | } 31 | 32 | // Sample is a single sample belonging to a metric. 33 | type Sample struct { 34 | Point 35 | Labels prommodel.LabelSet 36 | Window *time.Duration 37 | } 38 | 39 | // Point represents a single data point for a given timestamp. 40 | type Point struct { 41 | Timestamp prommodel.Time 42 | Value float64 43 | } 44 | -------------------------------------------------------------------------------- /logo/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.github/workflows/release-algorithm.yaml: -------------------------------------------------------------------------------- 1 | name: Release Algorithm 2 | on: 3 | push: 4 | tags: 5 | - 'algorithm-v*' 6 | permissions: 7 | contents: read 8 | packages: write 9 | jobs: 10 | build-and-push-image: 11 | name: Build and push image 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - name: Get tag 19 | run: echo "tag=$(git describe --tags --match 'algorithm-v*' | sed -e 's/^algorithm-//')" >> $GITHUB_ENV 20 | - name: Set up QEMU 21 | uses: docker/setup-qemu-action@v3 22 | - name: Set up Docker Buildx 23 | uses: docker/setup-buildx-action@v3 24 | - name: Login to GitHub Container Registry 25 | uses: docker/login-action@v3 26 | with: 27 | registry: ghcr.io 28 | username: ${{ github.actor }} 29 | password: ${{ secrets.GITHUB_TOKEN }} 30 | - name: Build and push image 31 | uses: docker/build-push-action@v6 32 | with: 33 | context: ./algorithm 34 | file: ./algorithm/horizontal-predictive.Dockerfile 35 | platforms: linux/amd64 36 | push: true 37 | tags: ghcr.io/${{ github.repository_owner }}/kapacity-algorithm-horizontal-predictive:${{ env.tag }} 38 | -------------------------------------------------------------------------------- /pkg/portrait/algorithm/externaljob/resultfetcher/interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package resultfetcher 18 | 19 | import ( 20 | "context" 21 | 22 | autoscalingv1alpha1 "github.com/traas-stack/kapacity/apis/autoscaling/v1alpha1" 23 | ) 24 | 25 | // Horizontal provides method to fetch the algorithm result of external horizontal portrait algorithm jobs. 26 | type Horizontal interface { 27 | // FetchResult fetches the latest algorithm result of the external horizontal portrait algorithm job managed by given HorizontalPortrait with given result source config. 28 | FetchResult(ctx context.Context, hp *autoscalingv1alpha1.HorizontalPortrait, cfg *autoscalingv1alpha1.PortraitAlgorithmResultSource) (*autoscalingv1alpha1.HorizontalPortraitData, error) 29 | } 30 | -------------------------------------------------------------------------------- /config/default/webhookcainjection_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. 3 | apiVersion: admissionregistration.k8s.io/v1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | labels: 7 | app.kubernetes.io/name: mutatingwebhookconfiguration 8 | app.kubernetes.io/instance: mutating-webhook-configuration 9 | app.kubernetes.io/component: webhook 10 | app.kubernetes.io/created-by: project-v3 11 | app.kubernetes.io/part-of: project-v3 12 | app.kubernetes.io/managed-by: kustomize 13 | name: mutating-webhook-configuration 14 | annotations: 15 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 16 | --- 17 | #apiVersion: admissionregistration.k8s.io/v1 18 | #kind: ValidatingWebhookConfiguration 19 | #metadata: 20 | # labels: 21 | # app.kubernetes.io/name: validatingwebhookconfiguration 22 | # app.kubernetes.io/instance: validating-webhook-configuration 23 | # app.kubernetes.io/component: webhook 24 | # app.kubernetes.io/created-by: project-v3 25 | # app.kubernetes.io/part-of: project-v3 26 | # app.kubernetes.io/managed-by: kustomize 27 | # name: validating-webhook-configuration 28 | # annotations: 29 | # cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 30 | -------------------------------------------------------------------------------- /pkg/workload/interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package workload 18 | 19 | import ( 20 | "context" 21 | 22 | corev1 "k8s.io/api/core/v1" 23 | 24 | podsorter "github.com/traas-stack/kapacity/pkg/pod/sorter" 25 | ) 26 | 27 | // Interface represent various behaviors of a specific Kubernetes workload. 28 | type Interface interface { 29 | // Interface provide the default pod scale down sort method of this workload. 30 | podsorter.Interface 31 | // CanSelectPodsToScaleDown means if this workload support scaling down specific pods. 32 | CanSelectPodsToScaleDown(context.Context) bool 33 | // SelectPodsToScaleDown select specific pods that would be scaled down when declining the replica of this workload. 34 | SelectPodsToScaleDown(context.Context, []*corev1.Pod) error 35 | } 36 | -------------------------------------------------------------------------------- /pkg/util/math_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | func TestMaxInt32(t *testing.T) { 26 | var min, max int32 = 1, 2 27 | value := MaxInt32(min, max) 28 | assert.Equal(t, max, value) 29 | } 30 | 31 | func TestMinInt32(t *testing.T) { 32 | var min, max int32 = 1, 2 33 | value := MinInt32(min, max) 34 | assert.Equal(t, min, value) 35 | } 36 | 37 | func TestAbsInt32(t *testing.T) { 38 | var positiveNum, negativeNum, zero int32 = 1, -1, 0 39 | 40 | value := AbsInt32(positiveNum) 41 | assert.Equal(t, positiveNum, value) 42 | 43 | value = AbsInt32(negativeNum) 44 | assert.Equal(t, -negativeNum, value) 45 | 46 | value = AbsInt32(zero) 47 | assert.Equal(t, zero, value) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/portrait/algorithm/externaljob/jobcontroller/interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package jobcontroller 18 | 19 | import ( 20 | "context" 21 | 22 | autoscalingv1alpha1 "github.com/traas-stack/kapacity/apis/autoscaling/v1alpha1" 23 | ) 24 | 25 | // Horizontal provides methods to manage external horizontal portrait algorithm jobs. 26 | type Horizontal interface { 27 | // UpdateJob creates or updates the external algorithm job managed by given HorizontalPortrait and job config. 28 | UpdateJob(ctx context.Context, hp *autoscalingv1alpha1.HorizontalPortrait, cfg *autoscalingv1alpha1.PortraitAlgorithmJob) error 29 | // CleanupJob does clean up works for the external algorithm job managed by given HorizontalPortrait. 30 | CleanupJob(ctx context.Context, hp *autoscalingv1alpha1.HorizontalPortrait) error 31 | } 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.19 as builder 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | WORKDIR /workspace 7 | # Copy the Go Modules manifests 8 | COPY go.mod go.mod 9 | COPY go.sum go.sum 10 | # cache deps before building and copying source so that we don't need to re-download as much 11 | # and so that source changes don't invalidate our downloaded layer 12 | RUN go mod download 13 | 14 | # Copy the go source 15 | COPY main.go main.go 16 | COPY apis/ apis/ 17 | COPY controllers/ controllers/ 18 | COPY internal/ internal/ 19 | COPY pkg/ pkg/ 20 | 21 | # Build 22 | # the GOARCH has not a default value to allow the binary be built according to the host where the command 23 | # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 24 | # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 25 | # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 26 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager main.go 27 | 28 | # Use distroless as minimal base image to package the manager binary 29 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 30 | FROM gcr.io/distroless/static:nonroot 31 | WORKDIR / 32 | COPY --from=builder /workspace/manager . 33 | USER 65532:65532 34 | 35 | ENTRYPOINT ["/manager"] 36 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/autoscaling.kapacitystack.io_replicaprofiles.yaml 6 | - bases/autoscaling.kapacitystack.io_intelligenthorizontalpodautoscalers.yaml 7 | - bases/autoscaling.kapacitystack.io_horizontalportraits.yaml 8 | #+kubebuilder:scaffold:crdkustomizeresource 9 | 10 | patchesStrategicMerge: 11 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 12 | # patches here are for enabling the conversion webhook for each CRD 13 | #- patches/webhook_in_replicaprofiles.yaml 14 | #- patches/webhook_in_intelligenthorizontalpodautoscalers.yaml 15 | #- patches/webhook_in_horizontalportraits.yaml 16 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 17 | 18 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 19 | # patches here are for enabling the CA injection for each CRD 20 | #- patches/cainjection_in_replicaprofiles.yaml 21 | #- patches/cainjection_in_intelligenthorizontalpodautoscalers.yaml 22 | #- patches/cainjection_in_horizontalportraits.yaml 23 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 24 | 25 | # the following config is for teaching kustomize how to do kustomization for CRDs. 26 | configurations: 27 | - kustomizeconfig.yaml 28 | -------------------------------------------------------------------------------- /apis/autoscaling/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the autoscaling v1alpha1 API group. 18 | // +kubebuilder:object:generate=true 19 | // +groupName=autoscaling.kapacitystack.io 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects. 29 | GroupVersion = schema.GroupVersion{Group: "autoscaling.kapacitystack.io", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme. 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /pkg/util/time.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "fmt" 21 | "time" 22 | 23 | "github.com/robfig/cron/v3" 24 | ) 25 | 26 | // IsCronActive returns if the given time is in the range specified by the start cron and end cron 27 | // as well as the next time of the end cron. 28 | func IsCronActive(t time.Time, startCron, endCron string) (bool, time.Time, error) { 29 | start, err := cron.ParseStandard(startCron) 30 | if err != nil { 31 | return false, time.Time{}, fmt.Errorf("failed to parse start cron %q: %v", startCron, err) 32 | } 33 | nextStart := start.Next(t) 34 | 35 | end, err := cron.ParseStandard(endCron) 36 | if err != nil { 37 | return false, time.Time{}, fmt.Errorf("failed to parse end cron %q: %v", endCron, err) 38 | } 39 | nextEnd := end.Next(t) 40 | 41 | return nextStart.After(nextEnd), nextEnd, nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/portrait/generator/interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package generator 18 | 19 | import ( 20 | "context" 21 | "time" 22 | 23 | k8sautoscalingv2 "k8s.io/api/autoscaling/v2" 24 | 25 | autoscalingv1alpha1 "github.com/traas-stack/kapacity/apis/autoscaling/v1alpha1" 26 | ) 27 | 28 | // Interface provide methods to generate portrait data. 29 | type Interface interface { 30 | // GenerateHorizontal portrait data for the scale target with specified metrics spec and algorithm configuration. 31 | // It returns the generated portrait data and an expected duration before next generating. 32 | GenerateHorizontal(ctx context.Context, namespace string, scaleTargetRef k8sautoscalingv2.CrossVersionObjectReference, metrics []autoscalingv1alpha1.MetricSpec, algorithm autoscalingv1alpha1.PortraitAlgorithm) (*autoscalingv1alpha1.HorizontalPortraitData, time.Duration, error) 33 | } 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a bug encountered while operating Kapacity. 3 | labels: kind/bug 4 | body: 5 | - type: textarea 6 | id: problem 7 | attributes: 8 | label: What happened? 9 | description: | 10 | Please provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. 11 | validations: 12 | required: true 13 | 14 | - type: textarea 15 | id: expected 16 | attributes: 17 | label: What did you expect to happen? 18 | validations: 19 | required: true 20 | 21 | - type: textarea 22 | id: repro 23 | attributes: 24 | label: How can we reproduce it (as minimally and precisely as possible)? 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | id: additional 30 | attributes: 31 | label: Anything else we need to know? 32 | 33 | - type: textarea 34 | id: kapacityVersion 35 | attributes: 36 | label: Kapacity version 37 | value: | 38 |
39 | 40 |
41 | validations: 42 | required: true 43 | 44 | - type: textarea 45 | id: kubeVersion 46 | attributes: 47 | label: Kubernetes version 48 | value: | 49 |
50 | 51 | ```console 52 | $ kubectl version 53 | # paste output here 54 | ``` 55 | 56 |
57 | validations: 58 | required: true -------------------------------------------------------------------------------- /pkg/metric/service/api/provider.proto: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | syntax = "proto3"; 18 | import "metric.proto"; 19 | import "google/protobuf/timestamp.proto"; 20 | import "google/protobuf/duration.proto"; 21 | 22 | package io.kapacitystack.metric; 23 | option go_package = "github.com/traas-stack/kapacity/pkg/metric/service/api"; 24 | 25 | service ProviderService { 26 | rpc QueryLatest (QueryLatestRequest) returns (QueryLatestResponse); 27 | rpc Query (QueryRequest) returns (QueryResponse); 28 | } 29 | 30 | message QueryLatestRequest { 31 | Query query = 1; 32 | } 33 | 34 | message QueryLatestResponse { 35 | repeated Sample samples = 1; 36 | } 37 | 38 | message QueryRequest { 39 | Query query = 1; 40 | google.protobuf.Timestamp start = 2; 41 | google.protobuf.Timestamp end = 3; 42 | google.protobuf.Duration step = 4; 43 | } 44 | 45 | message QueryResponse { 46 | repeated Series series = 1; 47 | } 48 | -------------------------------------------------------------------------------- /algorithm/kapacity/metric/pb/provider.proto: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | syntax = "proto3"; 18 | import "metric.proto"; 19 | import "google/protobuf/timestamp.proto"; 20 | import "google/protobuf/duration.proto"; 21 | 22 | package io.kapacitystack.metric; 23 | option go_package = "github.com/traas-stack/kapacity/pkg/metric/service/api"; 24 | 25 | service ProviderService { 26 | rpc QueryLatest (QueryLatestRequest) returns (QueryLatestResponse); 27 | rpc Query (QueryRequest) returns (QueryResponse); 28 | } 29 | 30 | message QueryLatestRequest { 31 | Query query = 1; 32 | } 33 | 34 | message QueryLatestResponse { 35 | repeated Sample samples = 1; 36 | } 37 | 38 | message QueryRequest { 39 | Query query = 1; 40 | google.protobuf.Timestamp start = 2; 41 | google.protobuf.Timestamp end = 3; 42 | google.protobuf.Duration step = 4; 43 | } 44 | 45 | message QueryResponse { 46 | repeated Series series = 1; 47 | } 48 | -------------------------------------------------------------------------------- /pkg/workload/deployment.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package workload 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | corev1 "k8s.io/api/core/v1" 24 | "k8s.io/apimachinery/pkg/labels" 25 | "sigs.k8s.io/controller-runtime/pkg/client" 26 | ) 27 | 28 | // Deployment represents behaviors of a Kubernetes Deployment. 29 | type Deployment struct { 30 | client.Client 31 | Namespace string 32 | Selector labels.Selector 33 | } 34 | 35 | func (w *Deployment) Sort(ctx context.Context, pods []*corev1.Pod) ([]*corev1.Pod, error) { 36 | rs := &ReplicaSet{ 37 | Client: w.Client, 38 | Namespace: w.Namespace, 39 | Selector: w.Selector, 40 | } 41 | return rs.Sort(ctx, pods) 42 | } 43 | 44 | func (*Deployment) CanSelectPodsToScaleDown(context.Context) bool { 45 | return false 46 | } 47 | 48 | func (*Deployment) SelectPodsToScaleDown(context.Context, []*corev1.Pod) error { 49 | return fmt.Errorf("Deployment does not support this operation") 50 | } 51 | -------------------------------------------------------------------------------- /pkg/workload/statefulset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package workload 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "sort" 23 | "strconv" 24 | "strings" 25 | 26 | corev1 "k8s.io/api/core/v1" 27 | ) 28 | 29 | // StatefulSet represents behaviors of a Kubernetes StatefulSet. 30 | type StatefulSet struct{} 31 | 32 | func (*StatefulSet) Sort(_ context.Context, pods []*corev1.Pod) ([]*corev1.Pod, error) { 33 | sort.Slice(pods, func(i, j int) bool { 34 | return getStatefulSetPodIndex(pods[i]) > getStatefulSetPodIndex(pods[j]) 35 | }) 36 | return pods, nil 37 | } 38 | 39 | func (*StatefulSet) CanSelectPodsToScaleDown(context.Context) bool { 40 | return false 41 | } 42 | 43 | func (*StatefulSet) SelectPodsToScaleDown(context.Context, []*corev1.Pod) error { 44 | return fmt.Errorf("StatefulSet does not support this operation") 45 | } 46 | 47 | func getStatefulSetPodIndex(pod *corev1.Pod) int { 48 | n, _ := strconv.Atoi(pod.Name[strings.LastIndex(pod.Name, "-")+1:]) 49 | return n 50 | } 51 | -------------------------------------------------------------------------------- /config/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | # WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. 4 | apiVersion: cert-manager.io/v1 5 | kind: Issuer 6 | metadata: 7 | labels: 8 | app.kubernetes.io/name: issuer 9 | app.kubernetes.io/instance: selfsigned-issuer 10 | app.kubernetes.io/component: certificate 11 | app.kubernetes.io/created-by: project-v3 12 | app.kubernetes.io/part-of: project-v3 13 | app.kubernetes.io/managed-by: kustomize 14 | name: selfsigned-issuer 15 | namespace: system 16 | spec: 17 | selfSigned: {} 18 | --- 19 | apiVersion: cert-manager.io/v1 20 | kind: Certificate 21 | metadata: 22 | labels: 23 | app.kubernetes.io/name: certificate 24 | app.kubernetes.io/instance: serving-cert 25 | app.kubernetes.io/component: certificate 26 | app.kubernetes.io/created-by: project-v3 27 | app.kubernetes.io/part-of: project-v3 28 | app.kubernetes.io/managed-by: kustomize 29 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 30 | namespace: system 31 | spec: 32 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize 33 | dnsNames: 34 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 35 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 36 | issuerRef: 37 | kind: Issuer 38 | name: selfsigned-issuer 39 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 40 | -------------------------------------------------------------------------------- /pkg/workload/deployment_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package workload 18 | 19 | import ( 20 | "context" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | corev1 "k8s.io/api/core/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | ) 27 | 28 | func TestDeployment_Sort(t *testing.T) { 29 | // TODO 30 | } 31 | 32 | func TestDeployment_CanSelectPodsToScaleDown(t *testing.T) { 33 | deployment := Deployment{} 34 | result := deployment.CanSelectPodsToScaleDown(context.Background()) 35 | assert.False(t, result, "can't select pods to scale down for deployment resource") 36 | } 37 | 38 | func TestDeployment_SelectPodsToScaleDown(t *testing.T) { 39 | pods := []*corev1.Pod{ 40 | { 41 | ObjectMeta: metav1.ObjectMeta{ 42 | Name: "pod-1", 43 | }, 44 | }, 45 | { 46 | ObjectMeta: metav1.ObjectMeta{ 47 | Name: "pod-2", 48 | }, 49 | }, 50 | } 51 | 52 | deployment := Deployment{} 53 | err := deployment.SelectPodsToScaleDown(context.Background(), pods) 54 | assert.NotNil(t, err, "does not support select pods for deployment resource") 55 | } 56 | -------------------------------------------------------------------------------- /pkg/workload/replicaset_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package workload 18 | 19 | import ( 20 | "context" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | corev1 "k8s.io/api/core/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | ) 27 | 28 | func TestReplicaSet_Sort(t *testing.T) { 29 | // TODO 30 | } 31 | 32 | func TestReplicaSet_CanSelectPodsToScaleDown(t *testing.T) { 33 | ReplicaSet := ReplicaSet{} 34 | result := ReplicaSet.CanSelectPodsToScaleDown(context.Background()) 35 | assert.False(t, result, "can't select pods to scale down for replicaset resource") 36 | } 37 | 38 | func TestReplicaSet_SelectPodsToScaleDown(t *testing.T) { 39 | pods := []*corev1.Pod{ 40 | { 41 | ObjectMeta: metav1.ObjectMeta{ 42 | Name: "pod-1", 43 | }, 44 | }, 45 | { 46 | ObjectMeta: metav1.ObjectMeta{ 47 | Name: "pod-2", 48 | }, 49 | }, 50 | } 51 | 52 | ReplicaSet := ReplicaSet{} 53 | err := ReplicaSet.SelectPodsToScaleDown(context.Background(), pods) 54 | assert.NotNil(t, err, "does not support select pods for replicaset resource") 55 | } 56 | -------------------------------------------------------------------------------- /internal/webhook/common/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package common 18 | 19 | import ( 20 | "fmt" 21 | 22 | ctrl "sigs.k8s.io/controller-runtime" 23 | "sigs.k8s.io/controller-runtime/pkg/webhook" 24 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 25 | ) 26 | 27 | // HandlerSetupFunc sets up an admission handler with the manager. 28 | type HandlerSetupFunc func(ctrl.Manager) (admission.Handler, error) 29 | 30 | // HandlerRegister contains information about how to register an admission handler 31 | // to a specific path of the webhook server. 32 | type HandlerRegister struct { 33 | Path string 34 | HandlerSetupFunc 35 | } 36 | 37 | // RegisterToServerWithManager sets up an admission handler and 38 | // registers it to the specified path of the webhook server. 39 | func (r *HandlerRegister) RegisterToServerWithManager(srv *webhook.Server, mgr ctrl.Manager) error { 40 | handler, err := r.HandlerSetupFunc(mgr) 41 | if err != nil { 42 | return fmt.Errorf("failed to set up handler: %v", err) 43 | } 44 | srv.Register(r.Path, &webhook.Admission{Handler: handler}) 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /pkg/util/common.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | prommodel "github.com/prometheus/common/model" 21 | ) 22 | 23 | const ( 24 | UserAgent = "kapacity-manager" 25 | ) 26 | 27 | // IsMapValueChanged compares two maps' values key by key, if any value in oldValues map differs 28 | // from the one in newValues map, it returns true, otherwise returns false. 29 | func IsMapValueChanged(oldValues, newValues map[string]string) bool { 30 | if len(newValues) == 0 { 31 | return false 32 | } 33 | 34 | for k, newV := range newValues { 35 | if oldV, ok := oldValues[k]; !ok || oldV != newV { 36 | return true 37 | } 38 | } 39 | 40 | return false 41 | } 42 | 43 | // CopyMapValues copies all the values from src map to dst map, overwriting any existing one. 44 | func CopyMapValues(dst, src map[string]string) { 45 | for k, v := range src { 46 | dst[k] = v 47 | } 48 | } 49 | 50 | func ConvertPromLabelSetToMap(in prommodel.LabelSet) map[string]string { 51 | out := make(map[string]string, len(in)) 52 | for k, v := range in { 53 | out[string(k)] = string(v) 54 | } 55 | return out 56 | } 57 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | affinity: 12 | nodeAffinity: 13 | requiredDuringSchedulingIgnoredDuringExecution: 14 | nodeSelectorTerms: 15 | - matchExpressions: 16 | - key: kubernetes.io/arch 17 | operator: In 18 | values: 19 | - amd64 20 | - arm64 21 | - ppc64le 22 | - s390x 23 | - key: kubernetes.io/os 24 | operator: In 25 | values: 26 | - linux 27 | containers: 28 | - name: kube-rbac-proxy 29 | securityContext: 30 | allowPrivilegeEscalation: false 31 | capabilities: 32 | drop: 33 | - "ALL" 34 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.0 35 | args: 36 | - "--secure-listen-address=0.0.0.0:8443" 37 | - "--upstream=http://127.0.0.1:8080/" 38 | - "--logtostderr=true" 39 | - "--v=0" 40 | ports: 41 | - containerPort: 8443 42 | protocol: TCP 43 | name: https 44 | resources: 45 | limits: 46 | cpu: 500m 47 | memory: 128Mi 48 | requests: 49 | cpu: 5m 50 | memory: 64Mi 51 | - name: manager 52 | args: 53 | - "--health-probe-bind-address=:8081" 54 | - "--metrics-bind-address=127.0.0.1:8080" 55 | - "--leader-elect" 56 | -------------------------------------------------------------------------------- /internal/webhook/pod/mutating/readinessgate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package mutating 18 | 19 | import ( 20 | admissionv1 "k8s.io/api/admission/v1" 21 | corev1 "k8s.io/api/core/v1" 22 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 23 | 24 | podtraffic "github.com/traas-stack/kapacity/pkg/pod/traffic" 25 | "github.com/traas-stack/kapacity/pkg/util" 26 | ) 27 | 28 | const ( 29 | labelInjectPodReadinessGate = "kapacitystack.io/inject-pod-readiness-gate" 30 | ) 31 | 32 | func injectReadinessGate(req admission.Request, pod *corev1.Pod) (changed bool) { 33 | if req.Operation != admissionv1.Create || req.SubResource != "" { 34 | return false 35 | } 36 | if pod.Labels[labelInjectPodReadinessGate] != "true" { 37 | return false 38 | } 39 | 40 | return util.AddPodReadinessGate(&pod.Spec, podtraffic.ReadinessGateOnline) 41 | } 42 | 43 | func defaultReadinessGateStatus(req admission.Request, pod *corev1.Pod) (changed bool) { 44 | if req.Operation != admissionv1.Update || req.SubResource != "status" { 45 | return false 46 | } 47 | if pod.Labels[labelInjectPodReadinessGate] != "true" { 48 | return false 49 | } 50 | 51 | return util.AddPodCondition(&pod.Status, &corev1.PodCondition{ 52 | Type: podtraffic.ReadinessGateOnline, 53 | Status: corev1.ConditionTrue, 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/util/wait.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | Copyright 2014 The Kubernetes Authors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package util 19 | 20 | import ( 21 | "context" 22 | "time" 23 | 24 | "k8s.io/apimachinery/pkg/util/runtime" 25 | "k8s.io/apimachinery/pkg/util/wait" 26 | ) 27 | 28 | // ExponentialBackoffWithContext works similar like wait.ExponentialBackoffWithContext but with below differences: 29 | // * It does not stop when the cap of backoff is reached. 30 | // * It does not return the error of ctx when done. 31 | func ExponentialBackoffWithContext(ctx context.Context, backoff wait.Backoff, condition wait.ConditionWithContextFunc) error { 32 | for { 33 | select { 34 | case <-ctx.Done(): 35 | return nil 36 | default: 37 | } 38 | 39 | if ok, err := runConditionWithCrashProtectionWithContext(ctx, condition); err != nil || ok { 40 | return err 41 | } 42 | 43 | t := time.NewTimer(backoff.Step()) 44 | select { 45 | case <-ctx.Done(): 46 | t.Stop() 47 | return nil 48 | case <-t.C: 49 | } 50 | } 51 | } 52 | 53 | // runConditionWithCrashProtectionWithContext is copied from k8s.io/apimachinery/pkg/util/wait. 54 | func runConditionWithCrashProtectionWithContext(ctx context.Context, condition wait.ConditionWithContextFunc) (bool, error) { 55 | defer runtime.HandleCrash() 56 | return condition(ctx) 57 | } 58 | -------------------------------------------------------------------------------- /pkg/util/common_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "testing" 21 | 22 | prommodel "github.com/prometheus/common/model" 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestIsMapValueChanged(t *testing.T) { 27 | oldValues := map[string]string{"key": "a"} 28 | 29 | // The new map is empty 30 | newValues := map[string]string{} 31 | assert.False(t, IsMapValueChanged(oldValues, newValues)) 32 | 33 | // The new map value is changed 34 | newValues["key"] = "b" 35 | assert.True(t, IsMapValueChanged(oldValues, newValues)) 36 | 37 | // The new map has new key-value pairs 38 | newValues = map[string]string{"new_key": "b"} 39 | assert.True(t, IsMapValueChanged(oldValues, newValues)) 40 | } 41 | 42 | func TestCopyMapValues(t *testing.T) { 43 | src := map[string]string{"key_1": "a", "key_2": "c"} 44 | dst := map[string]string{"key_1": "b"} 45 | CopyMapValues(dst, src) 46 | for k, v := range src { 47 | assert.Equal(t, v, dst[k]) 48 | } 49 | } 50 | 51 | func TestConvertPromLabelSetToMap(t *testing.T) { 52 | in := map[prommodel.LabelName]prommodel.LabelValue{"key_1": "a", "key_2": "b"} 53 | out := ConvertPromLabelSetToMap(in) 54 | assert.Equal(t, len(in), len(out)) 55 | for k, v := range out { 56 | assert.Equal(t, v, string(in[prommodel.LabelName(k)])) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /internal/grpc/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package grpc 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "net" 23 | 24 | "google.golang.org/grpc" 25 | "sigs.k8s.io/controller-runtime/pkg/manager" 26 | ) 27 | 28 | // Server is a leader election runnable gRPC server. 29 | type Server interface { 30 | manager.Runnable 31 | manager.LeaderElectionRunnable 32 | // ServiceRegistrar returns the gRPC service registrar of the server. 33 | ServiceRegistrar() grpc.ServiceRegistrar 34 | } 35 | 36 | // NewServer creates a new Server with the given bind address and gRPC server options. 37 | func NewServer(addr string, opts ...grpc.ServerOption) Server { 38 | return &server{ 39 | bindAddress: addr, 40 | grpcServer: grpc.NewServer(opts...), 41 | } 42 | } 43 | 44 | type server struct { 45 | bindAddress string 46 | grpcServer *grpc.Server 47 | } 48 | 49 | func (s *server) Start(context.Context) error { 50 | ln, err := net.Listen("tcp", s.bindAddress) 51 | if err != nil { 52 | return fmt.Errorf("failed to listening on %s: %v", s.bindAddress, err) 53 | } 54 | if err := s.grpcServer.Serve(ln); err != nil { 55 | return fmt.Errorf("failed to serve gRPC: %v", err) 56 | } 57 | return nil 58 | } 59 | 60 | func (*server) NeedLeaderElection() bool { 61 | return false 62 | } 63 | 64 | func (s *server) ServiceRegistrar() grpc.ServiceRegistrar { 65 | return s.grpcServer 66 | } 67 | -------------------------------------------------------------------------------- /pkg/portrait/provider/interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package provider 18 | 19 | import ( 20 | "context" 21 | 22 | autoscalingv1alpha1 "github.com/traas-stack/kapacity/apis/autoscaling/v1alpha1" 23 | ) 24 | 25 | // Horizontal provides methods to manage horizontal portraits and fetch value from them. 26 | type Horizontal interface { 27 | // GetPortraitIdentifier returns a string identifier of the portrait managed by given IHPA and provider config. 28 | GetPortraitIdentifier(ihpa *autoscalingv1alpha1.IntelligentHorizontalPodAutoscaler, cfg *autoscalingv1alpha1.HorizontalPortraitProvider) string 29 | // UpdatePortraitSpec creates or updates the portrait backend managed by given IHPA and provider config. 30 | UpdatePortraitSpec(ctx context.Context, ihpa *autoscalingv1alpha1.IntelligentHorizontalPodAutoscaler, cfg *autoscalingv1alpha1.HorizontalPortraitProvider) error 31 | // FetchPortraitValue fetches the current value from the data of portrait managed by given IHPA and provider config. 32 | FetchPortraitValue(ctx context.Context, ihpa *autoscalingv1alpha1.IntelligentHorizontalPodAutoscaler, cfg *autoscalingv1alpha1.HorizontalPortraitProvider) (*autoscalingv1alpha1.HorizontalPortraitValue, error) 33 | // CleanupPortrait does clean up works for the identified portrait managed by given IHPA. 34 | CleanupPortrait(ctx context.Context, ihpa *autoscalingv1alpha1.IntelligentHorizontalPodAutoscaler, identifier string) error 35 | } 36 | -------------------------------------------------------------------------------- /pkg/portrait/provider/static.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package provider 18 | 19 | import ( 20 | "context" 21 | 22 | autoscalingv1alpha1 "github.com/traas-stack/kapacity/apis/autoscaling/v1alpha1" 23 | ) 24 | 25 | // StaticHorizontal provides horizontal portraits with static replicas values. 26 | type StaticHorizontal struct{} 27 | 28 | // NewStaticHorizontal creates a new StaticHorizontal. 29 | func NewStaticHorizontal() Horizontal { 30 | return &StaticHorizontal{} 31 | } 32 | 33 | func (*StaticHorizontal) GetPortraitIdentifier(*autoscalingv1alpha1.IntelligentHorizontalPodAutoscaler, *autoscalingv1alpha1.HorizontalPortraitProvider) string { 34 | return string(autoscalingv1alpha1.StaticHorizontalPortraitProviderType) 35 | } 36 | 37 | func (*StaticHorizontal) UpdatePortraitSpec(context.Context, *autoscalingv1alpha1.IntelligentHorizontalPodAutoscaler, *autoscalingv1alpha1.HorizontalPortraitProvider) error { 38 | // do nothing 39 | return nil 40 | } 41 | 42 | func (h *StaticHorizontal) FetchPortraitValue(_ context.Context, ihpa *autoscalingv1alpha1.IntelligentHorizontalPodAutoscaler, cfg *autoscalingv1alpha1.HorizontalPortraitProvider) (*autoscalingv1alpha1.HorizontalPortraitValue, error) { 43 | return &autoscalingv1alpha1.HorizontalPortraitValue{ 44 | Provider: h.GetPortraitIdentifier(ihpa, cfg), 45 | Replicas: cfg.Static.Replicas, 46 | }, nil 47 | } 48 | 49 | func (*StaticHorizontal) CleanupPortrait(context.Context, *autoscalingv1alpha1.IntelligentHorizontalPodAutoscaler, string) error { 50 | // do nothing 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /algorithm/kapacity/metric/pb/provider_pb2.pyi: -------------------------------------------------------------------------------- 1 | import metric_pb2 as _metric_pb2 2 | from google.protobuf import timestamp_pb2 as _timestamp_pb2 3 | from google.protobuf import duration_pb2 as _duration_pb2 4 | from google.protobuf.internal import containers as _containers 5 | from google.protobuf import descriptor as _descriptor 6 | from google.protobuf import message as _message 7 | from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union 8 | 9 | DESCRIPTOR: _descriptor.FileDescriptor 10 | 11 | class QueryLatestRequest(_message.Message): 12 | __slots__ = ["query"] 13 | QUERY_FIELD_NUMBER: _ClassVar[int] 14 | query: _metric_pb2.Query 15 | def __init__(self, query: _Optional[_Union[_metric_pb2.Query, _Mapping]] = ...) -> None: ... 16 | 17 | class QueryLatestResponse(_message.Message): 18 | __slots__ = ["samples"] 19 | SAMPLES_FIELD_NUMBER: _ClassVar[int] 20 | samples: _containers.RepeatedCompositeFieldContainer[_metric_pb2.Sample] 21 | def __init__(self, samples: _Optional[_Iterable[_Union[_metric_pb2.Sample, _Mapping]]] = ...) -> None: ... 22 | 23 | class QueryRequest(_message.Message): 24 | __slots__ = ["query", "start", "end", "step"] 25 | QUERY_FIELD_NUMBER: _ClassVar[int] 26 | START_FIELD_NUMBER: _ClassVar[int] 27 | END_FIELD_NUMBER: _ClassVar[int] 28 | STEP_FIELD_NUMBER: _ClassVar[int] 29 | query: _metric_pb2.Query 30 | start: _timestamp_pb2.Timestamp 31 | end: _timestamp_pb2.Timestamp 32 | step: _duration_pb2.Duration 33 | def __init__(self, query: _Optional[_Union[_metric_pb2.Query, _Mapping]] = ..., start: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., end: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., step: _Optional[_Union[_duration_pb2.Duration, _Mapping]] = ...) -> None: ... 34 | 35 | class QueryResponse(_message.Message): 36 | __slots__ = ["series"] 37 | SERIES_FIELD_NUMBER: _ClassVar[int] 38 | series: _containers.RepeatedCompositeFieldContainer[_metric_pb2.Series] 39 | def __init__(self, series: _Optional[_Iterable[_Union[_metric_pb2.Series, _Mapping]]] = ...) -> None: ... 40 | -------------------------------------------------------------------------------- /pkg/util/client_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | corev1 "k8s.io/api/core/v1" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/labels" 26 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 27 | ) 28 | 29 | func TestCtrlPodLister(t *testing.T) { 30 | fakeClient := fake.NewClientBuilder().WithObjects( 31 | &corev1.Pod{ 32 | ObjectMeta: metav1.ObjectMeta{ 33 | Name: "pod1", 34 | Namespace: "ns1", 35 | Labels: map[string]string{ 36 | "key1": "value1", 37 | }, 38 | }, 39 | }, 40 | &corev1.Pod{ 41 | ObjectMeta: metav1.ObjectMeta{ 42 | Name: "pod2", 43 | Namespace: "ns1", 44 | Labels: map[string]string{ 45 | "key2": "value2", 46 | }, 47 | }, 48 | }, 49 | &corev1.Pod{ 50 | ObjectMeta: metav1.ObjectMeta{ 51 | Name: "pod3", 52 | Namespace: "ns2", 53 | Labels: map[string]string{ 54 | "key1": "value1", 55 | }, 56 | }, 57 | }, 58 | ).Build() 59 | lister := NewCtrlPodLister(fakeClient) 60 | 61 | pods, err := lister.List(labels.SelectorFromSet(map[string]string{"key1": "value1"})) 62 | assert.Nil(t, err) 63 | assert.Equal(t, 2, len(pods)) 64 | for _, pod := range pods { 65 | assert.NotEqual(t, "pod2", pod.Name) 66 | } 67 | 68 | pods, err = lister.Pods("ns1").List(labels.SelectorFromSet(map[string]string{"key1": "value1"})) 69 | assert.Nil(t, err) 70 | assert.Equal(t, 1, len(pods)) 71 | assert.Equal(t, "pod1", pods[0].Name) 72 | } 73 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | permissions: 5 | contents: read 6 | pull-requests: read 7 | jobs: 8 | lint: 9 | name: Run linting 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | - name: Get changed files 15 | id: changed-files 16 | uses: tj-actions/changed-files@v45 17 | with: 18 | files_yaml: | 19 | go: 20 | - '**/*.go' 21 | - name: Set up Go 22 | if: steps.changed-files.outputs.go_any_changed == 'true' 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version: "1.19" 26 | cache: false 27 | - name: Run golangci-lint 28 | if: steps.changed-files.outputs.go_any_changed == 'true' 29 | uses: golangci/golangci-lint-action@v6 30 | with: 31 | version: latest 32 | only-new-issues: true 33 | # TODO(zqzten): add verify job 34 | test: 35 | name: Run unit tests 36 | needs: lint 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout code 40 | uses: actions/checkout@v4 41 | with: 42 | fetch-depth: 0 43 | - name: Get changed files 44 | id: changed-files 45 | uses: tj-actions/changed-files@v45 46 | with: 47 | files_yaml: | 48 | go: 49 | - '**/*.go' 50 | - go.mod 51 | - go.sum 52 | - Makefile 53 | - name: Git config 54 | if: steps.changed-files.outputs.go_any_modified == 'true' 55 | run: | 56 | git config user.name "github-actions[bot]" 57 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 58 | - name: Rebase code 59 | if: steps.changed-files.outputs.go_any_modified == 'true' 60 | run: git rebase origin/main 61 | - name: Set up Go 62 | if: steps.changed-files.outputs.go_any_modified == 'true' 63 | uses: actions/setup-go@v5 64 | with: 65 | go-version: "1.19" 66 | - name: Run unit tests 67 | if: steps.changed-files.outputs.go_any_modified == 'true' 68 | run: make unit-test 69 | -------------------------------------------------------------------------------- /pkg/pod/traffic/readinessgate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package traffic 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | corev1 "k8s.io/api/core/v1" 24 | apiv1pod "k8s.io/kubernetes/pkg/api/v1/pod" 25 | "sigs.k8s.io/controller-runtime/pkg/client" 26 | ) 27 | 28 | const ( 29 | ReadinessGateOnline = "kapacitystack.io/online" 30 | ) 31 | 32 | // ReadinessGate controls pod traffic by setting a specific readiness gate to make the pod ready/unready 33 | // so that it would be automatically added to/removed from the endpoints of Kubernetes services. 34 | type ReadinessGate struct { 35 | client.Client 36 | } 37 | 38 | func (c *ReadinessGate) On(ctx context.Context, pods []*corev1.Pod) error { 39 | for _, pod := range pods { 40 | if err := c.setReadinessGateStatus(ctx, pod, corev1.ConditionTrue); err != nil { 41 | return fmt.Errorf("failed to set readiness gate of pod %q: %v", pod.Name, err) 42 | } 43 | } 44 | return nil 45 | } 46 | 47 | func (c *ReadinessGate) Off(ctx context.Context, pods []*corev1.Pod) error { 48 | for _, pod := range pods { 49 | if err := c.setReadinessGateStatus(ctx, pod, corev1.ConditionFalse); err != nil { 50 | return fmt.Errorf("failed to set readiness gate of pod %q: %v", pod.Name, err) 51 | } 52 | } 53 | return nil 54 | } 55 | 56 | func (c *ReadinessGate) setReadinessGateStatus(ctx context.Context, pod *corev1.Pod, status corev1.ConditionStatus) error { 57 | patch := client.MergeFrom(pod.DeepCopy()) 58 | if apiv1pod.UpdatePodCondition(&pod.Status, &corev1.PodCondition{ 59 | Type: ReadinessGateOnline, 60 | Status: status, 61 | }) { 62 | return c.Status().Patch(ctx, pod, patch) 63 | } 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /pkg/metric/provider/prometheus/resource.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | Copyright 2018 The Kubernetes Authors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package prometheus 19 | 20 | import ( 21 | "fmt" 22 | 23 | apimeta "k8s.io/apimachinery/pkg/api/meta" 24 | "sigs.k8s.io/prometheus-adapter/pkg/naming" 25 | ) 26 | 27 | // resourceQuery represents query information for querying resource metrics for some resource, like CPU or memory. 28 | type resourceQuery struct { 29 | ContainerQuery naming.MetricsQuery 30 | ReadyPodsOnlyContainerQuery naming.MetricsQuery 31 | ContainerLabel string 32 | } 33 | 34 | // newResourceQuery instantiates query information from the give configuration rule for querying 35 | // resource metrics for some resource. 36 | func newResourceQuery(cfg ResourceRule, mapper apimeta.RESTMapper) (*resourceQuery, error) { 37 | converter, err := naming.NewResourceConverter(cfg.Resources.Template, cfg.Resources.Overrides, mapper) 38 | if err != nil { 39 | return nil, fmt.Errorf("unable to construct label-resource converter: %v", err) 40 | } 41 | 42 | containerQuery, err := naming.NewMetricsQuery(cfg.ContainerQuery, converter) 43 | if err != nil { 44 | return nil, fmt.Errorf("unable to construct container metrics query: %v", err) 45 | } 46 | 47 | readyPodsOnlyContainerQuery, err := naming.NewMetricsQuery(cfg.ReadyPodsOnlyContainerQuery, converter) 48 | if err != nil { 49 | return nil, fmt.Errorf("unable to construct ready pods only container metrics query: %v", err) 50 | } 51 | 52 | return &resourceQuery{ 53 | ContainerQuery: containerQuery, 54 | ReadyPodsOnlyContainerQuery: readyPodsOnlyContainerQuery, 55 | ContainerLabel: cfg.ContainerLabel, 56 | }, nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/workload/statefulset_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package workload 18 | 19 | import ( 20 | "context" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | corev1 "k8s.io/api/core/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | ) 27 | 28 | func TestStatefulSet_Sort(t *testing.T) { 29 | pods := []*corev1.Pod{ 30 | { 31 | ObjectMeta: metav1.ObjectMeta{ 32 | Name: "pod-1", 33 | }, 34 | }, 35 | { 36 | ObjectMeta: metav1.ObjectMeta{ 37 | Name: "pod-3", 38 | }, 39 | }, 40 | { 41 | ObjectMeta: metav1.ObjectMeta{ 42 | Name: "pod-2", 43 | }, 44 | }, 45 | } 46 | 47 | statefulSet := StatefulSet{} 48 | sortedPods, err := statefulSet.Sort(context.Background(), pods) 49 | assert.Nil(t, err) 50 | 51 | // Names are in reverse order 52 | lastIndex := -1 53 | for _, pod := range sortedPods { 54 | index := getStatefulSetPodIndex(pod) 55 | if lastIndex > 0 { 56 | assert.Less(t, index, lastIndex, "pod index is incorrect for %s", pod.Name) 57 | } 58 | lastIndex = index 59 | } 60 | } 61 | 62 | func TestStatefulSet_CanSelectPodsToScaleDown(t *testing.T) { 63 | statefulSet := StatefulSet{} 64 | result := statefulSet.CanSelectPodsToScaleDown(context.Background()) 65 | assert.False(t, result, "can't select pods to scale down for statefulSet resource") 66 | } 67 | 68 | func TestStatefulSet_SelectPodsToScaleDown(t *testing.T) { 69 | pods := []*corev1.Pod{ 70 | { 71 | ObjectMeta: metav1.ObjectMeta{ 72 | Name: "pod-1", 73 | }, 74 | }, 75 | { 76 | ObjectMeta: metav1.ObjectMeta{ 77 | Name: "pod-2", 78 | }, 79 | }, 80 | } 81 | 82 | statefulSet := StatefulSet{} 83 | err := statefulSet.SelectPodsToScaleDown(context.Background(), pods) 84 | assert.NotNil(t, err, "does not support select pods for statefulSet resource") 85 | } 86 | -------------------------------------------------------------------------------- /controllers/autoscaling/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package autoscaling 18 | 19 | import ( 20 | "path/filepath" 21 | "testing" 22 | 23 | . "github.com/onsi/ginkgo/v2" 24 | . "github.com/onsi/gomega" 25 | 26 | "k8s.io/client-go/kubernetes/scheme" 27 | "k8s.io/client-go/rest" 28 | "sigs.k8s.io/controller-runtime/pkg/client" 29 | "sigs.k8s.io/controller-runtime/pkg/envtest" 30 | logf "sigs.k8s.io/controller-runtime/pkg/log" 31 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 32 | 33 | //+kubebuilder:scaffold:imports 34 | 35 | autoscalingv1alpha1 "github.com/traas-stack/kapacity/apis/autoscaling/v1alpha1" 36 | ) 37 | 38 | var cfg *rest.Config 39 | var k8sClient client.Client 40 | var testEnv *envtest.Environment 41 | 42 | func TestAPIs(t *testing.T) { 43 | RegisterFailHandler(Fail) 44 | 45 | RunSpecs(t, "Controller Suite") 46 | } 47 | 48 | var _ = BeforeSuite(func() { 49 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 50 | 51 | By("bootstrapping test environment") 52 | testEnv = &envtest.Environment{ 53 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, 54 | ErrorIfCRDPathMissing: true, 55 | } 56 | 57 | var err error 58 | // cfg is defined in this file globally. 59 | cfg, err = testEnv.Start() 60 | Expect(err).NotTo(HaveOccurred()) 61 | Expect(cfg).NotTo(BeNil()) 62 | 63 | err = autoscalingv1alpha1.AddToScheme(scheme.Scheme) 64 | Expect(err).NotTo(HaveOccurred()) 65 | 66 | //+kubebuilder:scaffold:scheme 67 | 68 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 69 | Expect(err).NotTo(HaveOccurred()) 70 | Expect(k8sClient).NotTo(BeNil()) 71 | 72 | // TODO: write actual env test 73 | }) 74 | 75 | var _ = AfterSuite(func() { 76 | By("tearing down the test environment") 77 | err := testEnv.Stop() 78 | Expect(err).NotTo(HaveOccurred()) 79 | }) 80 | -------------------------------------------------------------------------------- /pkg/workload/replicaset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | Copyright 2016 The Kubernetes Authors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package workload 19 | 20 | import ( 21 | "context" 22 | "fmt" 23 | "sort" 24 | 25 | corev1 "k8s.io/api/core/v1" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | "k8s.io/apimachinery/pkg/labels" 28 | "k8s.io/kubernetes/pkg/controller" 29 | "sigs.k8s.io/controller-runtime/pkg/client" 30 | 31 | "github.com/traas-stack/kapacity/pkg/util" 32 | ) 33 | 34 | // ReplicaSet represents behaviors of a Kubernetes ReplicaSet. 35 | type ReplicaSet struct { 36 | client.Client 37 | Namespace string 38 | Selector labels.Selector 39 | } 40 | 41 | func (w *ReplicaSet) Sort(ctx context.Context, pods []*corev1.Pod) ([]*corev1.Pod, error) { 42 | // related pods are all pods which selected by the ReplicaSet 43 | relatedPods := &corev1.PodList{} 44 | if err := w.List(ctx, relatedPods, client.InNamespace(w.Namespace), client.MatchingLabelsSelector{Selector: w.Selector}); err != nil { 45 | return nil, fmt.Errorf("failed to list related pods: %v", err) 46 | } 47 | // build rank equal to the number of active pods in related pods that are colocated on the same node with the pod 48 | podsOnNode := make(map[string]int) 49 | for i := range relatedPods.Items { 50 | pod := &relatedPods.Items[i] 51 | if util.IsPodActive(pod) { 52 | podsOnNode[pod.Spec.NodeName]++ 53 | } 54 | } 55 | ranks := make([]int, 0, len(pods)) 56 | for i, pod := range pods { 57 | ranks[i] = podsOnNode[pod.Spec.NodeName] 58 | } 59 | sort.Sort(controller.ActivePodsWithRanks{ 60 | Pods: pods, 61 | Rank: ranks, 62 | Now: metav1.Now(), 63 | }) 64 | return pods, nil 65 | } 66 | 67 | func (*ReplicaSet) CanSelectPodsToScaleDown(context.Context) bool { 68 | return false 69 | } 70 | 71 | func (*ReplicaSet) SelectPodsToScaleDown(context.Context, []*corev1.Pod) error { 72 | return fmt.Errorf("ReplicaSet does not support this operation") 73 | } 74 | -------------------------------------------------------------------------------- /pkg/portrait/provider/static_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package provider 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | 24 | autoscalingv1alpha1 "github.com/traas-stack/kapacity/apis/autoscaling/v1alpha1" 25 | ) 26 | 27 | var ( 28 | staticHorizontalProvider = &autoscalingv1alpha1.HorizontalPortraitProvider{ 29 | Type: autoscalingv1alpha1.StaticHorizontalPortraitProviderType, 30 | Static: &autoscalingv1alpha1.StaticHorizontalPortraitProvider{ 31 | Replicas: targetReplicas, 32 | }, 33 | } 34 | ) 35 | 36 | func TestStaticHorizontal_GetPortraitIdentifier(t *testing.T) { 37 | staticHorizontal := NewStaticHorizontal() 38 | portraitIdentifier := staticHorizontal.GetPortraitIdentifier(ihpa, staticHorizontalProvider) 39 | assert.Equal(t, string(staticHorizontalProvider.Type), portraitIdentifier) 40 | } 41 | 42 | func TestStaticHorizontal_UpdatePortraitSpec(t *testing.T) { 43 | staticHorizontal := NewStaticHorizontal() 44 | err := staticHorizontal.UpdatePortraitSpec(ctx, ihpa, staticHorizontalProvider) 45 | assert.Nil(t, err) 46 | } 47 | 48 | func TestStaticHorizontal_FetchPortraitValue(t *testing.T) { 49 | staticHorizontal := NewStaticHorizontal() 50 | err := staticHorizontal.UpdatePortraitSpec(ctx, ihpa, staticHorizontalProvider) 51 | assert.Nil(t, err) 52 | 53 | portraitValue, err := staticHorizontal.FetchPortraitValue(ctx, ihpa, staticHorizontalProvider) 54 | assert.Nil(t, err) 55 | assert.Equal(t, portraitValue.Provider, string(staticHorizontalProvider.Type)) 56 | assert.Equal(t, portraitValue.Replicas, staticHorizontalProvider.Static.Replicas) 57 | } 58 | 59 | func TestStaticHorizontal_CleanupPortrait(t *testing.T) { 60 | staticHorizontal := NewStaticHorizontal() 61 | err := staticHorizontal.UpdatePortraitSpec(ctx, ihpa, staticHorizontalProvider) 62 | assert.Nil(t, err) 63 | 64 | err = staticHorizontal.CleanupPortrait(ctx, ihpa, string(staticHorizontalProvider.Type)) 65 | assert.Nil(t, err) 66 | } 67 | -------------------------------------------------------------------------------- /pkg/util/pod.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | corev1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | apiv1pod "k8s.io/kubernetes/pkg/api/v1/pod" 23 | ) 24 | 25 | // GetPodNames generate a list of pod names from a list of pod objects. 26 | func GetPodNames(pods []*corev1.Pod) []string { 27 | result := make([]string, 0, len(pods)) 28 | for _, pod := range pods { 29 | result = append(result, pod.Name) 30 | } 31 | return result 32 | } 33 | 34 | // IsPodRunning returns if the given pod's phase is running and is not being deleted. 35 | func IsPodRunning(pod *corev1.Pod) bool { 36 | return pod.DeletionTimestamp.IsZero() && pod.Status.Phase == corev1.PodRunning 37 | } 38 | 39 | // IsPodActive returns if the given pod has not terminated. 40 | func IsPodActive(pod *corev1.Pod) bool { 41 | return pod.Status.Phase != corev1.PodSucceeded && 42 | pod.Status.Phase != corev1.PodFailed && 43 | pod.DeletionTimestamp.IsZero() 44 | } 45 | 46 | // AddPodCondition adds a pod condition if not exists. Sets LastTransitionTime to now if not exists. 47 | // Returns true if pod condition has been added. 48 | func AddPodCondition(status *corev1.PodStatus, condition *corev1.PodCondition) bool { 49 | if _, oldCondition := apiv1pod.GetPodCondition(status, condition.Type); oldCondition != nil { 50 | return false 51 | } 52 | condition.LastTransitionTime = metav1.Now() 53 | status.Conditions = append(status.Conditions, *condition) 54 | return true 55 | } 56 | 57 | // AddPodReadinessGate adds the provided condition to the pod's readiness gates. 58 | // Returns true if the readiness gate has been added. 59 | func AddPodReadinessGate(spec *corev1.PodSpec, conditionType corev1.PodConditionType) bool { 60 | for _, rg := range spec.ReadinessGates { 61 | if rg.ConditionType == conditionType { 62 | return false 63 | } 64 | } 65 | spec.ReadinessGates = append(spec.ReadinessGates, corev1.PodReadinessGate{ConditionType: conditionType}) 66 | return true 67 | } 68 | -------------------------------------------------------------------------------- /internal/webhook/pod/mutating/handler.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package mutating 18 | 19 | import ( 20 | "context" 21 | "encoding/json" 22 | "fmt" 23 | "net/http" 24 | 25 | corev1 "k8s.io/api/core/v1" 26 | ctrl "sigs.k8s.io/controller-runtime" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 29 | 30 | "github.com/traas-stack/kapacity/internal/webhook/common" 31 | ) 32 | 33 | // +kubebuilder:webhook:path=/mutate-v1-pod,mutating=true,failurePolicy=fail,sideEffects=None,admissionReviewVersions=v1;v1beta1,groups="",resources=pods;pods/status,verbs=create;update,versions=v1,name=mpod.kb.io 34 | 35 | var HandlerRegister = &common.HandlerRegister{ 36 | Path: "/mutate-v1-pod", 37 | HandlerSetupFunc: func(mgr ctrl.Manager) (admission.Handler, error) { 38 | decoder, err := admission.NewDecoder(mgr.GetScheme()) 39 | if err != nil { 40 | return nil, fmt.Errorf("failed to new admission decoder: %v", err) 41 | } 42 | return &Handler{ 43 | Client: mgr.GetClient(), 44 | Decoder: decoder, 45 | }, nil 46 | }, 47 | } 48 | 49 | type Handler struct { 50 | client.Client 51 | *admission.Decoder 52 | } 53 | 54 | func (h *Handler) Handle(_ context.Context, req admission.Request) admission.Response { 55 | pod := &corev1.Pod{} 56 | if err := h.Decode(req, pod); err != nil { 57 | return admission.Errored(http.StatusBadRequest, fmt.Errorf("failed to decode obj: %v", err)) 58 | } 59 | 60 | var needMutate bool 61 | 62 | if changed := injectReadinessGate(req, pod); changed { 63 | needMutate = true 64 | } 65 | if changed := defaultReadinessGateStatus(req, pod); changed { 66 | needMutate = true 67 | } 68 | 69 | if !needMutate { 70 | return admission.Allowed("") 71 | } 72 | marshaled, err := json.Marshal(pod) 73 | if err != nil { 74 | return admission.Errored(http.StatusInternalServerError, fmt.Errorf("failed to marshal mutated pod: %v", err)) 75 | } 76 | return admission.PatchResponseFromRaw(req.Object.Raw, marshaled) 77 | } 78 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: kapacity-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: kapacity- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | bases: 16 | - ../crd 17 | - ../rbac 18 | - ../manager 19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 20 | # crd/kustomization.yaml 21 | - ../webhook 22 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 23 | - ../certmanager 24 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 25 | #- ../prometheus 26 | 27 | patchesStrategicMerge: 28 | # Protect the /metrics endpoint by putting it behind auth. 29 | # If you want your controller-manager to expose the /metrics 30 | # endpoint w/o any authn/z, please comment the following line. 31 | #- manager_auth_proxy_patch.yaml 32 | 33 | 34 | 35 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 36 | # crd/kustomization.yaml 37 | - manager_webhook_patch.yaml 38 | 39 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 40 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 41 | # 'CERTMANAGER' needs to be enabled to use ca injection 42 | - webhookcainjection_patch.yaml 43 | 44 | # the following config is for teaching kustomize how to do var substitution 45 | vars: 46 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 47 | - name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 48 | objref: 49 | kind: Certificate 50 | group: cert-manager.io 51 | version: v1 52 | name: serving-cert # this name should match the one in certificate.yaml 53 | fieldref: 54 | fieldpath: metadata.namespace 55 | - name: CERTIFICATE_NAME 56 | objref: 57 | kind: Certificate 58 | group: cert-manager.io 59 | version: v1 60 | name: serving-cert # this name should match the one in certificate.yaml 61 | - name: SERVICE_NAMESPACE # namespace of the service 62 | objref: 63 | kind: Service 64 | version: v1 65 | name: webhook-service 66 | fieldref: 67 | fieldpath: metadata.namespace 68 | - name: SERVICE_NAME 69 | objref: 70 | kind: Service 71 | version: v1 72 | name: webhook-service 73 | -------------------------------------------------------------------------------- /pkg/metric/provider/prometheus/external_series_registry.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | Copyright 2017 The Kubernetes Authors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package prometheus 19 | 20 | import ( 21 | "fmt" 22 | "sync" 23 | 24 | "k8s.io/apimachinery/pkg/labels" 25 | promadapterclient "sigs.k8s.io/prometheus-adapter/pkg/client" 26 | "sigs.k8s.io/prometheus-adapter/pkg/naming" 27 | ) 28 | 29 | // externalSeriesRegistry acts as a low-level converter for transforming external metrics queries 30 | // into Prometheus queries. 31 | type externalSeriesRegistry struct { 32 | mu sync.RWMutex 33 | // info maps metrics to information about the corresponding series 34 | info map[string]seriesInfo 35 | } 36 | 37 | func (r *externalSeriesRegistry) QueryForMetric(namespace string, metricName string, metricSelector labels.Selector) (string, error) { 38 | r.mu.RLock() 39 | defer r.mu.RUnlock() 40 | 41 | info, found := r.info[metricName] 42 | if !found { 43 | return "", fmt.Errorf("external metric %q not found", metricName) 44 | } 45 | 46 | query, err := info.Namer.QueryForExternalSeries(info.SeriesName, namespace, metricSelector) 47 | return string(query), err 48 | } 49 | 50 | // SetSeries replaces the known series in registry. 51 | // Each slice in series should correspond to a naming.MetricNamer in namers. 52 | func (r *externalSeriesRegistry) SetSeries(newSeriesSlices [][]promadapterclient.Series, namers []naming.MetricNamer) error { 53 | if len(newSeriesSlices) != len(namers) { 54 | return fmt.Errorf("need one set of series per namer") 55 | } 56 | 57 | newInfo := make(map[string]seriesInfo) 58 | for i, newSeries := range newSeriesSlices { 59 | namer := namers[i] 60 | for _, series := range newSeries { 61 | name, err := namer.MetricNameForSeries(series) 62 | if err != nil { 63 | metricsListerLog.Error(err, "unable to name series, skipping", "series", series.String()) 64 | continue 65 | } 66 | newInfo[name] = seriesInfo{ 67 | SeriesName: series.Name, 68 | Namer: namer, 69 | } 70 | } 71 | } 72 | 73 | r.mu.Lock() 74 | defer r.mu.Unlock() 75 | r.info = newInfo 76 | 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: namespace 7 | app.kubernetes.io/instance: system 8 | app.kubernetes.io/component: manager 9 | app.kubernetes.io/created-by: kapacity 10 | app.kubernetes.io/part-of: kapacity 11 | app.kubernetes.io/managed-by: kustomize 12 | name: system 13 | --- 14 | apiVersion: apps/v1 15 | kind: Deployment 16 | metadata: 17 | name: controller-manager 18 | namespace: system 19 | labels: 20 | control-plane: controller-manager 21 | app.kubernetes.io/name: deployment 22 | app.kubernetes.io/instance: controller-manager 23 | app.kubernetes.io/component: manager 24 | app.kubernetes.io/created-by: kapacity 25 | app.kubernetes.io/part-of: kapacity 26 | app.kubernetes.io/managed-by: kustomize 27 | spec: 28 | selector: 29 | matchLabels: 30 | control-plane: controller-manager 31 | replicas: 1 32 | template: 33 | metadata: 34 | annotations: 35 | kubectl.kubernetes.io/default-container: manager 36 | labels: 37 | control-plane: controller-manager 38 | spec: 39 | securityContext: 40 | runAsNonRoot: true 41 | containers: 42 | - command: 43 | - /manager 44 | args: 45 | - --zap-stacktrace-level=panic 46 | - --zap-log-level=1 47 | - --leader-elect 48 | - --prometheus-metrics-config=/etc/kapacity/prometheus-metrics-config.yaml 49 | - --algorithm-job-namespace=kapacity-system 50 | - --algorithm-job-default-service-account=kapacity-algorithm-job 51 | - --algorithm-job-default-metrics-server-addr=kapacity-grpc-service:9090 52 | image: controller:latest 53 | name: manager 54 | securityContext: 55 | allowPrivilegeEscalation: false 56 | capabilities: 57 | drop: 58 | - ALL 59 | livenessProbe: 60 | httpGet: 61 | path: /healthz 62 | port: 8081 63 | initialDelaySeconds: 15 64 | periodSeconds: 20 65 | readinessProbe: 66 | httpGet: 67 | path: /readyz 68 | port: 8081 69 | initialDelaySeconds: 5 70 | periodSeconds: 10 71 | ports: 72 | - containerPort: 9090 73 | name: grpc-server 74 | protocol: TCP 75 | volumeMounts: 76 | - name: config 77 | mountPath: /etc/kapacity 78 | readOnly: true 79 | serviceAccountName: controller-manager 80 | terminationGracePeriodSeconds: 10 81 | volumes: 82 | - name: config 83 | configMap: 84 | name: kapacity-config 85 | -------------------------------------------------------------------------------- /config/manager/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: config 5 | namespace: system 6 | labels: 7 | control-plane: controller-manager 8 | app.kubernetes.io/name: configmap 9 | app.kubernetes.io/instance: config 10 | app.kubernetes.io/component: manager 11 | app.kubernetes.io/created-by: kapacity 12 | app.kubernetes.io/part-of: kapacity 13 | app.kubernetes.io/managed-by: kustomize 14 | data: 15 | prometheus-metrics-config.yaml: | 16 | resourceRules: 17 | cpu: 18 | containerQuery: |- 19 | sum by (<<.GroupBy>>) ( 20 | irate(container_cpu_usage_seconds_total{container!="",container!="POD",<<.LabelMatchers>>}[3m]) 21 | ) 22 | readyPodsOnlyContainerQuery: |- 23 | sum by (<<.GroupBy>>) ( 24 | (kube_pod_status_ready{condition="true"} == 1) 25 | * on (namespace, pod) group_left () 26 | sum by (namespace, pod) ( 27 | irate(container_cpu_usage_seconds_total{container!="",container!="POD",<<.LabelMatchers>>}[3m]) 28 | ) 29 | ) 30 | resources: 31 | overrides: 32 | namespace: 33 | resource: namespace 34 | pod: 35 | resource: pod 36 | containerLabel: container 37 | memory: 38 | containerQuery: |- 39 | sum by (<<.GroupBy>>) ( 40 | container_memory_working_set_bytes{container!="",container!="POD",<<.LabelMatchers>>} 41 | ) 42 | readyPodsOnlyContainerQuery: |- 43 | sum by (<<.GroupBy>>) ( 44 | (kube_pod_status_ready{condition="true"} == 1) 45 | * on (namespace, pod) group_left () 46 | sum by (namespace, pod) ( 47 | container_memory_working_set_bytes{container!="",container!="POD",<<.LabelMatchers>>} 48 | ) 49 | ) 50 | resources: 51 | overrides: 52 | namespace: 53 | resource: namespace 54 | pod: 55 | resource: pod 56 | containerLabel: container 57 | window: 3m 58 | externalRules: 59 | - seriesQuery: '{__name__="kube_pod_status_ready"}' 60 | metricsQuery: sum(<<.Series>>{condition="true",<<.LabelMatchers>>}) 61 | name: 62 | as: ready_pods_count 63 | resources: 64 | overrides: 65 | namespace: 66 | resource: namespace 67 | workloadPodNamePatterns: 68 | - group: apps 69 | kind: ReplicaSet 70 | pattern: ^%s-[a-z0-9]+$ 71 | - group: apps 72 | kind: Deployment 73 | pattern: ^%s-[a-z0-9]+-[a-z0-9]+$ 74 | - group: apps 75 | kind: StatefulSet 76 | pattern: ^%s-[0-9]+$ 77 | -------------------------------------------------------------------------------- /pkg/util/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "context" 21 | 22 | corev1 "k8s.io/api/core/v1" 23 | "k8s.io/apimachinery/pkg/labels" 24 | "k8s.io/apimachinery/pkg/types" 25 | corev1listers "k8s.io/client-go/listers/core/v1" 26 | "sigs.k8s.io/controller-runtime/pkg/client" 27 | ) 28 | 29 | // NewCtrlPodLister creates a corev1listers.PodLister wrapper for given controller-runtime client. 30 | func NewCtrlPodLister(client client.Client) corev1listers.PodLister { 31 | return &ctrlPodLister{ 32 | client: client, 33 | } 34 | } 35 | 36 | type ctrlPodLister struct { 37 | client client.Client 38 | } 39 | 40 | type ctrlPodNamespaceLister struct { 41 | client client.Client 42 | namespace string 43 | } 44 | 45 | func (l *ctrlPodLister) List(selector labels.Selector) ([]*corev1.Pod, error) { 46 | podList := &corev1.PodList{} 47 | if err := l.client.List(context.TODO(), podList, client.MatchingLabelsSelector{Selector: selector}); err != nil { 48 | return nil, err 49 | } 50 | return convertPodListToPointerSlice(podList), nil 51 | } 52 | 53 | func (l *ctrlPodLister) Pods(namespace string) corev1listers.PodNamespaceLister { 54 | return &ctrlPodNamespaceLister{ 55 | client: l.client, 56 | namespace: namespace, 57 | } 58 | } 59 | 60 | func (l *ctrlPodNamespaceLister) List(selector labels.Selector) ([]*corev1.Pod, error) { 61 | podList := &corev1.PodList{} 62 | if err := l.client.List(context.TODO(), podList, client.InNamespace(l.namespace), client.MatchingLabelsSelector{Selector: selector}); err != nil { 63 | return nil, err 64 | } 65 | return convertPodListToPointerSlice(podList), nil 66 | } 67 | 68 | func (l *ctrlPodNamespaceLister) Get(name string) (*corev1.Pod, error) { 69 | pod := &corev1.Pod{} 70 | if err := l.client.Get(context.TODO(), types.NamespacedName{Namespace: l.namespace, Name: name}, pod); err != nil { 71 | return nil, err 72 | } 73 | return pod, nil 74 | } 75 | 76 | func convertPodListToPointerSlice(podList *corev1.PodList) []*corev1.Pod { 77 | s := make([]*corev1.Pod, len(podList.Items)) 78 | for i := range podList.Items { 79 | s[i] = &podList.Items[i] 80 | } 81 | return s 82 | } 83 | -------------------------------------------------------------------------------- /algorithm/kapacity/metric/pb/provider_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: provider.proto 4 | """Generated protocol buffer code.""" 5 | from google.protobuf import descriptor as _descriptor 6 | from google.protobuf import descriptor_pool as _descriptor_pool 7 | from google.protobuf import symbol_database as _symbol_database 8 | from google.protobuf.internal import builder as _builder 9 | # @@protoc_insertion_point(imports) 10 | 11 | _sym_db = _symbol_database.Default() 12 | 13 | 14 | from . import metric_pb2 as metric__pb2 15 | from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 16 | from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 17 | 18 | 19 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0eprovider.proto\x12\x17io.kapacitystack.metric\x1a\x0cmetric.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\"C\n\x12QueryLatestRequest\x12-\n\x05query\x18\x01 \x01(\x0b\x32\x1e.io.kapacitystack.metric.Query\"G\n\x13QueryLatestResponse\x12\x30\n\x07samples\x18\x01 \x03(\x0b\x32\x1f.io.kapacitystack.metric.Sample\"\xba\x01\n\x0cQueryRequest\x12-\n\x05query\x18\x01 \x01(\x0b\x32\x1e.io.kapacitystack.metric.Query\x12)\n\x05start\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\'\n\x03\x65nd\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\'\n\x04step\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\"@\n\rQueryResponse\x12/\n\x06series\x18\x01 \x03(\x0b\x32\x1f.io.kapacitystack.metric.Series2\xd3\x01\n\x0fProviderService\x12h\n\x0bQueryLatest\x12+.io.kapacitystack.metric.QueryLatestRequest\x1a,.io.kapacitystack.metric.QueryLatestResponse\x12V\n\x05Query\x12%.io.kapacitystack.metric.QueryRequest\x1a&.io.kapacitystack.metric.QueryResponseB8Z6github.com/traas-stack/kapacity/pkg/metric/service/apib\x06proto3') 20 | 21 | _globals = globals() 22 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 23 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'provider_pb2', _globals) 24 | if _descriptor._USE_C_DESCRIPTORS == False: 25 | 26 | DESCRIPTOR._options = None 27 | DESCRIPTOR._serialized_options = b'Z6github.com/traas-stack/kapacity/pkg/metric/service/api' 28 | _globals['_QUERYLATESTREQUEST']._serialized_start=122 29 | _globals['_QUERYLATESTREQUEST']._serialized_end=189 30 | _globals['_QUERYLATESTRESPONSE']._serialized_start=191 31 | _globals['_QUERYLATESTRESPONSE']._serialized_end=262 32 | _globals['_QUERYREQUEST']._serialized_start=265 33 | _globals['_QUERYREQUEST']._serialized_end=451 34 | _globals['_QUERYRESPONSE']._serialized_start=453 35 | _globals['_QUERYRESPONSE']._serialized_end=517 36 | _globals['_PROVIDERSERVICE']._serialized_start=520 37 | _globals['_PROVIDERSERVICE']._serialized_end=731 38 | # @@protoc_insertion_point(module_scope) 39 | -------------------------------------------------------------------------------- /pkg/portrait/provider/cron.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package provider 18 | 19 | import ( 20 | "context" 21 | 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | "k8s.io/apimachinery/pkg/types" 24 | "sigs.k8s.io/controller-runtime/pkg/event" 25 | 26 | autoscalingv1alpha1 "github.com/traas-stack/kapacity/apis/autoscaling/v1alpha1" 27 | ) 28 | 29 | // CronHorizontal provides horizontal portraits with replicas values based on cron rules. 30 | type CronHorizontal struct { 31 | cronTaskTriggerManager *cronTaskTriggerManager 32 | } 33 | 34 | // NewCronHorizontal creates a new CronHorizontal with the given event trigger. 35 | func NewCronHorizontal(eventTrigger chan event.GenericEvent) Horizontal { 36 | return &CronHorizontal{ 37 | cronTaskTriggerManager: newCronTaskTriggerManager(eventTrigger), 38 | } 39 | } 40 | 41 | func (*CronHorizontal) GetPortraitIdentifier(*autoscalingv1alpha1.IntelligentHorizontalPodAutoscaler, *autoscalingv1alpha1.HorizontalPortraitProvider) string { 42 | return string(autoscalingv1alpha1.CronHorizontalPortraitProviderType) 43 | } 44 | 45 | func (h *CronHorizontal) UpdatePortraitSpec(_ context.Context, ihpa *autoscalingv1alpha1.IntelligentHorizontalPodAutoscaler, cfg *autoscalingv1alpha1.HorizontalPortraitProvider) error { 46 | return h.cronTaskTriggerManager.StartCronTaskTrigger(types.NamespacedName{Namespace: ihpa.Namespace, Name: ihpa.Name}, ihpa, cfg.Cron.Crons) 47 | } 48 | 49 | func (h *CronHorizontal) FetchPortraitValue(_ context.Context, ihpa *autoscalingv1alpha1.IntelligentHorizontalPodAutoscaler, cfg *autoscalingv1alpha1.HorizontalPortraitProvider) (*autoscalingv1alpha1.HorizontalPortraitValue, error) { 50 | rc, expireTime, err := getActiveReplicaCron(cfg.Cron.Crons) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | if rc == nil { 56 | return nil, nil 57 | } 58 | 59 | return &autoscalingv1alpha1.HorizontalPortraitValue{ 60 | Provider: h.GetPortraitIdentifier(ihpa, cfg), 61 | Replicas: rc.Replicas, 62 | ExpireTime: &metav1.Time{Time: expireTime}, 63 | }, nil 64 | } 65 | 66 | func (h *CronHorizontal) CleanupPortrait(_ context.Context, ihpa *autoscalingv1alpha1.IntelligentHorizontalPodAutoscaler, _ string) error { 67 | h.cronTaskTriggerManager.StopCronTaskTrigger(types.NamespacedName{Namespace: ihpa.Namespace, Name: ihpa.Name}) 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /pkg/util/apimachinery.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | Copyright 2015 The Kubernetes Authors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package util 19 | 20 | import ( 21 | "fmt" 22 | 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "k8s.io/apimachinery/pkg/labels" 25 | "k8s.io/utils/pointer" 26 | "sigs.k8s.io/controller-runtime/pkg/client" 27 | ) 28 | 29 | // SetConditionInList sets the specific condition type on the given condition list to the specified value with the given reason and message. 30 | // The condition will be added if it is not present. The new list will be returned. 31 | func SetConditionInList(inputList []metav1.Condition, conditionType string, status metav1.ConditionStatus, observedGeneration int64, reason, message string) []metav1.Condition { 32 | resList := inputList 33 | var existingCond *metav1.Condition 34 | for i, condition := range resList { 35 | if condition.Type == conditionType { 36 | existingCond = &resList[i] 37 | break 38 | } 39 | } 40 | 41 | if existingCond == nil { 42 | resList = append(resList, metav1.Condition{ 43 | Type: conditionType, 44 | }) 45 | existingCond = &resList[len(resList)-1] 46 | } 47 | 48 | if existingCond.Status != status { 49 | existingCond.LastTransitionTime = metav1.Now() 50 | } 51 | 52 | existingCond.Status = status 53 | existingCond.ObservedGeneration = observedGeneration 54 | existingCond.Reason = reason 55 | existingCond.Message = message 56 | 57 | return resList 58 | } 59 | 60 | // NewControllerRef creates a controller owner reference pointing to the given owner. 61 | func NewControllerRef(obj client.Object) *metav1.OwnerReference { 62 | return &metav1.OwnerReference{ 63 | APIVersion: obj.GetObjectKind().GroupVersionKind().GroupVersion().String(), 64 | Kind: obj.GetObjectKind().GroupVersionKind().Kind, 65 | Name: obj.GetName(), 66 | UID: obj.GetUID(), 67 | Controller: pointer.Bool(true), 68 | BlockOwnerDeletion: pointer.Bool(true), 69 | } 70 | } 71 | 72 | // ParseScaleSelector parses the selector string got from Kubernetes scale API to labels.Selector. 73 | func ParseScaleSelector(selector string) (labels.Selector, error) { 74 | if selector == "" { 75 | return nil, fmt.Errorf("selector should not be empty") 76 | } 77 | parsedSelector, err := labels.Parse(selector) 78 | if err != nil { 79 | return nil, fmt.Errorf("failed to convert selector into a corresponding internal selector object: %v", err) 80 | } 81 | return parsedSelector, nil 82 | } 83 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | creationTimestamp: null 6 | name: manager-role 7 | rules: 8 | - apiGroups: 9 | - "" 10 | resources: 11 | - configmaps 12 | verbs: 13 | - get 14 | - list 15 | - watch 16 | - apiGroups: 17 | - "" 18 | resources: 19 | - events 20 | verbs: 21 | - create 22 | - delete 23 | - get 24 | - list 25 | - patch 26 | - update 27 | - watch 28 | - apiGroups: 29 | - "" 30 | resources: 31 | - pods 32 | verbs: 33 | - get 34 | - list 35 | - patch 36 | - watch 37 | - apiGroups: 38 | - "" 39 | resources: 40 | - pods/status 41 | verbs: 42 | - get 43 | - patch 44 | - apiGroups: 45 | - '*' 46 | resources: 47 | - '*/scale' 48 | verbs: 49 | - get 50 | - update 51 | - apiGroups: 52 | - autoscaling.kapacitystack.io 53 | resources: 54 | - horizontalportraits 55 | verbs: 56 | - create 57 | - delete 58 | - get 59 | - list 60 | - patch 61 | - update 62 | - watch 63 | - apiGroups: 64 | - autoscaling.kapacitystack.io 65 | resources: 66 | - horizontalportraits/finalizers 67 | verbs: 68 | - update 69 | - apiGroups: 70 | - autoscaling.kapacitystack.io 71 | resources: 72 | - horizontalportraits/status 73 | verbs: 74 | - get 75 | - patch 76 | - update 77 | - apiGroups: 78 | - autoscaling.kapacitystack.io 79 | resources: 80 | - intelligenthorizontalpodautoscalers 81 | verbs: 82 | - create 83 | - delete 84 | - get 85 | - list 86 | - patch 87 | - update 88 | - watch 89 | - apiGroups: 90 | - autoscaling.kapacitystack.io 91 | resources: 92 | - intelligenthorizontalpodautoscalers/finalizers 93 | verbs: 94 | - update 95 | - apiGroups: 96 | - autoscaling.kapacitystack.io 97 | resources: 98 | - intelligenthorizontalpodautoscalers/status 99 | verbs: 100 | - get 101 | - patch 102 | - update 103 | - apiGroups: 104 | - autoscaling.kapacitystack.io 105 | resources: 106 | - replicaprofiles 107 | verbs: 108 | - create 109 | - delete 110 | - get 111 | - list 112 | - patch 113 | - update 114 | - watch 115 | - apiGroups: 116 | - autoscaling.kapacitystack.io 117 | resources: 118 | - replicaprofiles/finalizers 119 | verbs: 120 | - update 121 | - apiGroups: 122 | - autoscaling.kapacitystack.io 123 | resources: 124 | - replicaprofiles/status 125 | verbs: 126 | - get 127 | - patch 128 | - update 129 | - apiGroups: 130 | - batch 131 | resources: 132 | - cronjobs 133 | verbs: 134 | - create 135 | - delete 136 | - get 137 | - list 138 | - patch 139 | - watch 140 | - apiGroups: 141 | - custom.metrics.k8s.io 142 | resources: 143 | - '*' 144 | verbs: 145 | - get 146 | - list 147 | - watch 148 | - apiGroups: 149 | - external.metrics.k8s.io 150 | resources: 151 | - '*' 152 | verbs: 153 | - get 154 | - list 155 | - watch 156 | - apiGroups: 157 | - metrics.k8s.io 158 | resources: 159 | - '*' 160 | verbs: 161 | - get 162 | - list 163 | - watch 164 | -------------------------------------------------------------------------------- /pkg/portrait/algorithm/externaljob/resultfetcher/configmap_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package resultfetcher 18 | 19 | import ( 20 | "context" 21 | "testing" 22 | "time" 23 | 24 | "github.com/stretchr/testify/assert" 25 | corev1 "k8s.io/api/core/v1" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | "k8s.io/apimachinery/pkg/runtime" 28 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 29 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 30 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 31 | "sigs.k8s.io/controller-runtime/pkg/controller/controllertest" 32 | 33 | autoscalingv1alpha1 "github.com/traas-stack/kapacity/apis/autoscaling/v1alpha1" 34 | ) 35 | 36 | const ( 37 | testHpNamespace = "test-ns" 38 | testHpName = "test" 39 | ) 40 | 41 | var ( 42 | scheme = runtime.NewScheme() 43 | ) 44 | 45 | func init() { 46 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 47 | } 48 | 49 | func TestConfigMapHorizontal_FetchResult(t *testing.T) { 50 | fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&corev1.ConfigMap{ 51 | ObjectMeta: metav1.ObjectMeta{ 52 | Namespace: testHpNamespace, 53 | Name: testHpName + configMapNameSuffix, 54 | }, 55 | Data: map[string]string{ 56 | configMapKeyType: string(autoscalingv1alpha1.TimeSeriesHorizontalPortraitDataType), 57 | configMapKeyExpireTime: "2023-09-21T12:05:51Z", 58 | configMapKeyTimeSeries: `{"1695290760":3,"1695290880":6,"1695291000":9}`, 59 | }, 60 | }).Build() 61 | 62 | configMapHorizontal := NewConfigMapHorizontal(fakeClient, nil, &controllertest.FakeInformer{}) 63 | hpData, err := configMapHorizontal.FetchResult(context.Background(), &autoscalingv1alpha1.HorizontalPortrait{ 64 | ObjectMeta: metav1.ObjectMeta{ 65 | Namespace: testHpNamespace, 66 | Name: testHpName, 67 | }, 68 | }, &autoscalingv1alpha1.PortraitAlgorithmResultSource{}) 69 | 70 | assert.Nil(t, err) 71 | assert.Equal(t, &autoscalingv1alpha1.HorizontalPortraitData{ 72 | Type: autoscalingv1alpha1.TimeSeriesHorizontalPortraitDataType, 73 | TimeSeries: &autoscalingv1alpha1.TimeSeriesHorizontalPortraitData{ 74 | TimeSeries: []autoscalingv1alpha1.ReplicaTimeSeriesPoint{ 75 | { 76 | Timestamp: 1695290760, 77 | Replicas: 3, 78 | }, 79 | { 80 | Timestamp: 1695290880, 81 | Replicas: 6, 82 | }, 83 | { 84 | Timestamp: 1695291000, 85 | Replicas: 9, 86 | }, 87 | }, 88 | }, 89 | ExpireTime: &metav1.Time{Time: time.Date(2023, 9, 21, 12, 5, 51, 0, time.UTC)}, 90 | }, hpData) 91 | } 92 | -------------------------------------------------------------------------------- /pkg/util/pod_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | corev1 "k8s.io/api/core/v1" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | ) 26 | 27 | func TestGetPodNames(t *testing.T) { 28 | pods := []*corev1.Pod{ 29 | { 30 | ObjectMeta: metav1.ObjectMeta{ 31 | Name: "pod-1", 32 | }, 33 | }, 34 | { 35 | ObjectMeta: metav1.ObjectMeta{ 36 | Name: "pod-2", 37 | }, 38 | }, 39 | } 40 | 41 | podNames := GetPodNames(pods) 42 | for i, pod := range pods { 43 | assert.Equal(t, pod.Name, podNames[i]) 44 | } 45 | } 46 | 47 | func TestIsPodRunning(t *testing.T) { 48 | // running pod 49 | runningPod := &corev1.Pod{ 50 | ObjectMeta: metav1.ObjectMeta{ 51 | Name: "pod-1", 52 | }, 53 | Status: corev1.PodStatus{ 54 | Phase: corev1.PodRunning, 55 | }, 56 | } 57 | isRunning := IsPodRunning(runningPod) 58 | assert.True(t, isRunning) 59 | 60 | // pending pod 61 | pendingPod := &corev1.Pod{ 62 | ObjectMeta: metav1.ObjectMeta{ 63 | Name: "pod-2", 64 | }, 65 | Status: corev1.PodStatus{ 66 | Phase: corev1.PodPending, 67 | }, 68 | } 69 | isRunning = IsPodRunning(pendingPod) 70 | assert.False(t, isRunning) 71 | } 72 | 73 | func TestIsPodActive(t *testing.T) { 74 | // running pod 75 | runningPod := &corev1.Pod{ 76 | ObjectMeta: metav1.ObjectMeta{ 77 | Name: "pod-1", 78 | }, 79 | Status: corev1.PodStatus{ 80 | Phase: corev1.PodRunning, 81 | }, 82 | } 83 | assert.True(t, IsPodActive(runningPod)) 84 | 85 | // failed pod 86 | failedPod := &corev1.Pod{ 87 | ObjectMeta: metav1.ObjectMeta{ 88 | Name: "pod-2", 89 | }, 90 | Status: corev1.PodStatus{ 91 | Phase: corev1.PodFailed, 92 | }, 93 | } 94 | assert.False(t, IsPodActive(failedPod)) 95 | } 96 | 97 | func TestAddPodCondition(t *testing.T) { 98 | podStatus := &corev1.PodStatus{} 99 | condition := &corev1.PodCondition{ 100 | Type: corev1.PodReady, 101 | Status: corev1.ConditionFalse, 102 | Reason: "PodReady", 103 | } 104 | 105 | assert.True(t, AddPodCondition(podStatus, condition)) 106 | assert.False(t, AddPodCondition(podStatus, condition)) 107 | } 108 | 109 | func TestAddPodReadinessGate(t *testing.T) { 110 | podSpec := &corev1.PodSpec{} 111 | conditionType := corev1.PodReady 112 | 113 | assert.True(t, AddPodReadinessGate(podSpec, conditionType)) 114 | assert.False(t, AddPodReadinessGate(podSpec, conditionType)) 115 | } 116 | -------------------------------------------------------------------------------- /pkg/portrait/provider/cron_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package provider 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/mitchellh/hashstructure/v2" 23 | "github.com/stretchr/testify/assert" 24 | 25 | autoscalingv1alpha1 "github.com/traas-stack/kapacity/apis/autoscaling/v1alpha1" 26 | ) 27 | 28 | var ( 29 | cronHorizontalProvider = &autoscalingv1alpha1.HorizontalPortraitProvider{ 30 | Type: autoscalingv1alpha1.CronHorizontalPortraitProviderType, 31 | Cron: &autoscalingv1alpha1.CronHorizontalPortraitProvider{ 32 | Crons: crons, 33 | }, 34 | } 35 | ) 36 | 37 | func TestCronHorizontal_GetPortraitIdentifier(t *testing.T) { 38 | cronHorizontal := NewCronHorizontal(genericEvent) 39 | portraitIdentifier := cronHorizontal.GetPortraitIdentifier(ihpa, cronHorizontalProvider) 40 | assert.Equal(t, string(cronHorizontalProvider.Type), portraitIdentifier) 41 | } 42 | 43 | func TestCronHorizontal_UpdatePortraitSpec(t *testing.T) { 44 | horizontal := NewCronHorizontal(genericEvent) 45 | cronHorizontal := horizontal.(*CronHorizontal) 46 | assert.Nil(t, cronHorizontal.UpdatePortraitSpec(ctx, ihpa, cronHorizontalProvider)) 47 | defer func() { 48 | assert.Nil(t, cronHorizontal.CleanupPortrait(ctx, ihpa, string(cronHorizontalProvider.Type))) 49 | }() 50 | 51 | taskV, ok := cronHorizontal.cronTaskTriggerManager.cronTaskMap.Load(namespaceName) 52 | assert.True(t, ok) 53 | 54 | hash, err := hashstructure.Hash(crons, hashstructure.FormatV2, nil) 55 | assert.Nil(t, err) 56 | 57 | task := taskV.(*cronTask) 58 | assert.True(t, task.Hash == hash) 59 | } 60 | 61 | func TestCronHorizontal_FetchPortraitValue(t *testing.T) { 62 | horizontal := NewCronHorizontal(genericEvent) 63 | cronHorizontal := horizontal.(*CronHorizontal) 64 | assert.Nil(t, cronHorizontal.UpdatePortraitSpec(ctx, ihpa, cronHorizontalProvider)) 65 | defer func() { 66 | assert.Nil(t, cronHorizontal.CleanupPortrait(ctx, ihpa, string(cronHorizontalProvider.Type))) 67 | }() 68 | 69 | portraitValue, err := cronHorizontal.FetchPortraitValue(ctx, ihpa, cronHorizontalProvider) 70 | assert.Nil(t, err) 71 | assert.Equal(t, portraitValue.Replicas, cronHorizontalProvider.Cron.Crons[0].Replicas) 72 | assert.Equal(t, portraitValue.Provider, string(cronHorizontalProvider.Type)) 73 | } 74 | 75 | func TestCronHorizontal_CleanupPortrait(t *testing.T) { 76 | horizontal := NewCronHorizontal(genericEvent) 77 | cronHorizontal := horizontal.(*CronHorizontal) 78 | assert.Nil(t, cronHorizontal.UpdatePortraitSpec(ctx, ihpa, cronHorizontalProvider)) 79 | assert.Nil(t, cronHorizontal.CleanupPortrait(ctx, ihpa, string(cronHorizontalProvider.Type))) 80 | 81 | _, ok := cronHorizontal.cronTaskTriggerManager.cronTaskMap.Load(namespaceName) 82 | assert.False(t, ok) 83 | } 84 | -------------------------------------------------------------------------------- /pkg/util/apimachinery_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | 25 | "github.com/traas-stack/kapacity/apis/autoscaling/v1alpha1" 26 | ) 27 | 28 | var ( 29 | observedGeneration int64 = 1 30 | horizontalPortrait = &v1alpha1.HorizontalPortrait{ 31 | TypeMeta: metav1.TypeMeta{ 32 | Kind: "HorizontalPortrait", 33 | APIVersion: "autoscaling.kapacitystack.io/v1alpha1", 34 | }, 35 | ObjectMeta: metav1.ObjectMeta{ 36 | Name: "hp", 37 | }, 38 | } 39 | ) 40 | 41 | func TestSetConditionInList_EmptyConditionList(t *testing.T) { 42 | inputList := make([]metav1.Condition, 0) 43 | successGeneratePortrait := "SucceededGeneratePortrait" 44 | conditionType := string(v1alpha1.PortraitGenerated) 45 | conditionStatus := metav1.ConditionTrue 46 | 47 | conditionList := SetConditionInList(inputList, conditionType, conditionStatus, observedGeneration, successGeneratePortrait, "") 48 | if len(conditionList) != 1 { 49 | t.Errorf("condition size unexpected, expetcted 1, actaul %d", len(conditionList)) 50 | } 51 | 52 | assert.True(t, len(conditionList) == 1) 53 | 54 | condition := conditionList[0] 55 | assert.Equal(t, conditionType, condition.Type) 56 | assert.Equal(t, conditionStatus, condition.Status) 57 | assert.Equal(t, successGeneratePortrait, condition.Reason) 58 | } 59 | 60 | func TestSetConditionInList_ExistConditionType(t *testing.T) { 61 | conditionReason := "SucceededGeneratePortrait" 62 | conditionType := string(v1alpha1.PortraitGenerated) 63 | conditionStatus := metav1.ConditionTrue 64 | inputList := []metav1.Condition{ 65 | { 66 | Type: conditionType, 67 | Status: metav1.ConditionFalse, 68 | }, 69 | } 70 | 71 | conditionList := SetConditionInList(inputList, conditionType, conditionStatus, observedGeneration, conditionReason, "") 72 | assert.Equal(t, 1, len(conditionList)) 73 | 74 | condition := conditionList[0] 75 | assert.Equal(t, conditionType, condition.Type) 76 | assert.Equal(t, conditionStatus, condition.Status) 77 | assert.Equal(t, conditionReason, condition.Reason) 78 | } 79 | 80 | func TestNewControllerRef(t *testing.T) { 81 | ownerRef := NewControllerRef(horizontalPortrait) 82 | assert.Equal(t, horizontalPortrait.APIVersion, ownerRef.APIVersion) 83 | assert.Equal(t, horizontalPortrait.Kind, ownerRef.Kind) 84 | assert.Equal(t, horizontalPortrait.Name, ownerRef.Name) 85 | assert.Equal(t, horizontalPortrait.UID, ownerRef.UID) 86 | assert.Equal(t, true, *ownerRef.Controller) 87 | assert.Equal(t, true, *ownerRef.BlockOwnerDeletion) 88 | } 89 | 90 | func TestParseScaleSelector(t *testing.T) { 91 | labelSelector := "key=value" 92 | ls, err := ParseScaleSelector(labelSelector) 93 | assert.Nil(t, err) 94 | assert.Equal(t, labelSelector, ls.String()) 95 | } 96 | -------------------------------------------------------------------------------- /pkg/metric/service/api/metric.proto: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | syntax = "proto3"; 18 | import "google/protobuf/duration.proto"; 19 | 20 | package io.kapacitystack.metric; 21 | option go_package = "github.com/traas-stack/kapacity/pkg/metric/service/api"; 22 | 23 | message Series { 24 | repeated Point points = 1; 25 | map labels = 2; 26 | optional google.protobuf.Duration window = 3; 27 | } 28 | 29 | message Sample { 30 | Point point = 1; 31 | map labels = 2; 32 | optional google.protobuf.Duration window = 3; 33 | } 34 | 35 | message Point { 36 | int64 timestamp = 1; 37 | double value = 2; 38 | } 39 | 40 | message Query { 41 | QueryType type = 1; 42 | oneof query { 43 | PodResourceQuery pod_resource = 2; 44 | ContainerResourceQuery container_resource = 3; 45 | WorkloadResourceQuery workload_resource = 4; 46 | WorkloadContainerResourceQuery workload_container_resource = 5; 47 | ObjectQuery object = 6; 48 | ExternalQuery external = 7; 49 | WorkloadExternalQuery workload_external = 8; 50 | } 51 | } 52 | 53 | enum QueryType { 54 | POD_RESOURCE = 0; 55 | CONTAINER_RESOURCE = 1; 56 | WORKLOAD_RESOURCE = 2; 57 | WORKLOAD_CONTAINER_RESOURCE = 3; 58 | OBJECT = 4; 59 | EXTERNAL = 5; 60 | WORKLOAD_EXTERNAL = 6; 61 | } 62 | 63 | message PodResourceQuery { 64 | string namespace = 1; 65 | oneof pod_identifier { 66 | string name = 2; 67 | string selector = 3; 68 | } 69 | string resource_name = 4; 70 | } 71 | 72 | message ContainerResourceQuery { 73 | string namespace = 1; 74 | oneof pod_identifier { 75 | string name = 2; 76 | string selector = 3; 77 | } 78 | string resource_name = 4; 79 | string container_name = 5; 80 | } 81 | 82 | message WorkloadResourceQuery { 83 | GroupKind group_kind = 1; 84 | string namespace = 2; 85 | string name = 3; 86 | string resource_name = 4; 87 | bool ready_pods_only = 5; 88 | } 89 | 90 | message WorkloadContainerResourceQuery { 91 | GroupKind group_kind = 1; 92 | string namespace = 2; 93 | string name = 3; 94 | string resource_name = 4; 95 | string container_name = 5; 96 | bool ready_pods_only = 6; 97 | } 98 | 99 | message ObjectQuery { 100 | GroupKind group_kind = 1; 101 | optional string namespace = 2; 102 | oneof object_identifier { 103 | string name = 3; 104 | string selector = 4; 105 | } 106 | MetricIdentifier metric = 5; 107 | } 108 | 109 | message ExternalQuery { 110 | optional string namespace = 1 ; 111 | MetricIdentifier metric = 2; 112 | } 113 | 114 | message WorkloadExternalQuery { 115 | GroupKind group_kind = 1; 116 | string namespace = 2; 117 | string name = 3; 118 | MetricIdentifier metric = 4; 119 | } 120 | 121 | message GroupKind { 122 | string group = 1; 123 | string kind = 2; 124 | } 125 | 126 | message MetricIdentifier { 127 | string name = 1; 128 | optional string selector = 2; 129 | } 130 | -------------------------------------------------------------------------------- /algorithm/kapacity/metric/pb/metric.proto: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | syntax = "proto3"; 18 | import "google/protobuf/duration.proto"; 19 | 20 | package io.kapacitystack.metric; 21 | option go_package = "github.com/traas-stack/kapacity/pkg/metric/service/api"; 22 | 23 | message Series { 24 | repeated Point points = 1; 25 | map labels = 2; 26 | optional google.protobuf.Duration window = 3; 27 | } 28 | 29 | message Sample { 30 | Point point = 1; 31 | map labels = 2; 32 | optional google.protobuf.Duration window = 3; 33 | } 34 | 35 | message Point { 36 | int64 timestamp = 1; 37 | double value = 2; 38 | } 39 | 40 | message Query { 41 | QueryType type = 1; 42 | oneof query { 43 | PodResourceQuery pod_resource = 2; 44 | ContainerResourceQuery container_resource = 3; 45 | WorkloadResourceQuery workload_resource = 4; 46 | WorkloadContainerResourceQuery workload_container_resource = 5; 47 | ObjectQuery object = 6; 48 | ExternalQuery external = 7; 49 | WorkloadExternalQuery workload_external = 8; 50 | } 51 | } 52 | 53 | enum QueryType { 54 | POD_RESOURCE = 0; 55 | CONTAINER_RESOURCE = 1; 56 | WORKLOAD_RESOURCE = 2; 57 | WORKLOAD_CONTAINER_RESOURCE = 3; 58 | OBJECT = 4; 59 | EXTERNAL = 5; 60 | WORKLOAD_EXTERNAL = 6; 61 | } 62 | 63 | message PodResourceQuery { 64 | string namespace = 1; 65 | oneof pod_identifier { 66 | string name = 2; 67 | string selector = 3; 68 | } 69 | string resource_name = 4; 70 | } 71 | 72 | message ContainerResourceQuery { 73 | string namespace = 1; 74 | oneof pod_identifier { 75 | string name = 2; 76 | string selector = 3; 77 | } 78 | string resource_name = 4; 79 | string container_name = 5; 80 | } 81 | 82 | message WorkloadResourceQuery { 83 | GroupKind group_kind = 1; 84 | string namespace = 2; 85 | string name = 3; 86 | string resource_name = 4; 87 | bool ready_pods_only = 5; 88 | } 89 | 90 | message WorkloadContainerResourceQuery { 91 | GroupKind group_kind = 1; 92 | string namespace = 2; 93 | string name = 3; 94 | string resource_name = 4; 95 | string container_name = 5; 96 | bool ready_pods_only = 6; 97 | } 98 | 99 | message ObjectQuery { 100 | GroupKind group_kind = 1; 101 | optional string namespace = 2; 102 | oneof object_identifier { 103 | string name = 3; 104 | string selector = 4; 105 | } 106 | MetricIdentifier metric = 5; 107 | } 108 | 109 | message ExternalQuery { 110 | optional string namespace = 1 ; 111 | MetricIdentifier metric = 2; 112 | } 113 | 114 | message WorkloadExternalQuery { 115 | GroupKind group_kind = 1; 116 | string namespace = 2; 117 | string name = 3; 118 | MetricIdentifier metric = 4; 119 | } 120 | 121 | message GroupKind { 122 | string group = 1; 123 | string kind = 2; 124 | } 125 | 126 | message MetricIdentifier { 127 | string name = 1; 128 | optional string selector = 2; 129 | } 130 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | logo 5 | 6 | 7 | 8 | [![Go Reference](https://pkg.go.dev/badge/github.com/traas-stack/kapacity.svg)](https://pkg.go.dev/github.com/traas-stack/kapacity) 9 | [![License](https://img.shields.io/github/license/traas-stack/kapacity)](https://www.apache.org/licenses/LICENSE-2.0.html) 10 | ![GoVersion](https://img.shields.io/github/go-mod/go-version/traas-stack/kapacity) 11 | [![Go Report Card](https://goreportcard.com/badge/github.com/traas-stack/kapacity)](https://goreportcard.com/report/github.com/traas-stack/kapacity) 12 | 13 | > [English](README.md) | 中文 14 | 15 | --- 16 | 17 | ♻️ Kapacity 旨在为用户提供一套具备完善技术风险能力的、智能且开放的云原生容量技术,帮助用户安全稳定地实现极致降本增效,解决容量相关问题。 18 | 19 | Kapacity 基于蚂蚁集团内部容量系统的核心理念和多年的大规模生产实践经验而构建,该内部容量系统目前已能安全稳定地为蚂蚁持续节省年均约 10w 核的算力成本,同时,Kapacity 也结合了来自云原生社区的最佳实践。 20 | 21 | ✨ _观看我们在 KubeCon China 2023 上的中文演讲「[我们如何构建生产级 HPA:从智能算法到无风险自动扩缩容](https://mp.weixin.qq.com/s/TKWZhOZxAhB8HiwB2jAuvg)」来深入了解 Kapacity Intelligent HPA 的设计思想和实现原理!_ 22 | 23 | 🚀 _Kapacity 目前仍处于快速迭代的阶段,部分规划中的功能还未完全实现。如果你有任何需求或疑问,欢迎通过[社区](#社区与支持)和我们直接沟通。_ 24 | 25 | --- 26 | 27 | ## 核心能力 28 | 29 | ### Intelligent HPA 30 | 31 | [Kubernetes HPA](https://kubernetes.io/zh-cn/docs/tasks/run-application/horizontal-pod-autoscale/) 是一项用于对云原生工作负载进行自动扩缩容的常见技术,但它在实际大规模生产使用上的效果和实用度上却并不理想,主要由以下几个原因导致: 32 | 33 | * HPA 的自动扩缩容通过响应式的方式驱动,仅当应用负载已经超出设定水位时才会触发扩容,此时容量风险已经出现,只能起到应急的作用而非提前规避风险,尤其对于自身启动时间较长的应用,几乎起不到快速应对流量洪峰的作用。 34 | * HPA 通过简单的指标折比来计算扩缩容目标副本数,只适用于应用副本数和相关指标呈严格线性相关的理想场景,但实际生产当中应用的各类指标和副本数之间存在错综复杂的关系,该算法很难得到符合容量水位要求的副本数。 35 | * 容量弹性作为变更故障率较高的一类场景,HPA 除了支持限制扩缩容速率外没有提供任何其他的风险防控手段,在稳定性要求较高的生产环境中大规模落地是很难令人放心的。 36 | * HPA 作为 Kubernetes 内置能力,一方面自然有其开箱即用的好处,但另一方面也使其绑定在了具体的 K8s 版本上,自身的行为难以被用户扩展或调整,难以满足各类用户在不同应用场景下的定制化需求。 37 | 38 | 为此,我们构建了 Intelligent HPA (IHPA),它是一个更加智能化的、具备完善技术风险能力的且高度可扩展定制的 HPA 替代方案。它具有如下几个核心特性: 39 | 40 | * **通过多种可按需组合或定制的智能算法来驱动自动扩缩容** 41 | * 预测式算法:通过多指标时序预测,并结合对组分流量和应用容量及其对应副本数的综合建模,推理得出应用未来的推荐副本数。该算法能够很好地应对生产上多周期流量、趋势变化流量、多条流量共同影响容量、容量与副本数呈非线性关系等复杂场景,通用性和准确性兼具。 42 | * 突增式算法:通过对近一段时间的指标进行持续监控分析,快速发现潜在的流量异常或容量恶化情况,在容量风险实际发生前主动响应,及时进行扩容等操作以规避风险。 43 | * 此外,也内置了传统的响应式折比算法和基于定时规则进行扩缩容的能力。 44 | * **具备多种变更风险防控能力** 45 | * 支持在整个弹性过程中精细化地控制工作负载下每一个 Pod 的状态,比如仅摘除 Pod 流量或释放 Pod 计算资源但不实际终止或删除 Pod 等,实现多阶段缩容。通过这种灵活的 Pod 状态转换能够显著提升弹性效率并降低弹性风险。 46 | * 执行扩缩容时支持采用灰自定义度分批的变更策略,最大程度地减小了弹性变更的爆炸半径;同时还支持和上面的精细化 Pod 状态控制能力相结合来实现多阶段灰度,提升应急回滚速度,进一步降低弹性变更风险。 47 | * 支持用户自定义的变更期稳定性检查,包括自定义指标(不局限于用于弹性伸缩的指标)异常判断等,多维度地分析变更状况,一旦发现异常支持自动采取应急熔断措施,如变更暂停或变更回滚,真正做到弹性变更常态化无人值守。 48 | * **开放可扩展的架构** 49 | * 整个 IHPA 能力拆分为了管控、决策、执行三大模块,任一模块都可以做替换或扩展。 50 | * 提供了大量扩展点,使得其行为能够被用户自由扩展或调整。可扩展的部分包括但不限于自定义 Pod 摘挂流的逻辑、自定义 Pod 缩容优先级、自定义变更期稳定性检查逻辑等。 51 | 52 | ## 开始使用 Kapacity 53 | 54 | 访问 [kapacity.netlify.app](https://kapacity.netlify.app/zh-cn/) 来查阅官方文档。 55 | 56 | 或者跟随[快速开始教程](https://kapacity.netlify.app/zh-cn/docs/getting-started/)来快速入门。 57 | 58 | ## 社区与支持 59 | 60 | 如果你有任何问题或想法,可以通过下面的途径得到帮助与反馈: 61 | 62 | * 想询问一些通用的或使用上的问题,或者有任何想法?→ [GitHub Discussions](https://github.com/traas-stack/kapacity/discussions) 63 | * 想上报一个 BUG 或提交一个需求?→ [GitHub Issues](https://github.com/traas-stack/kapacity/issues) 64 | * 希望更深入地交流?欢迎通过下面的任意渠道加入我们的社区讨论: 65 | * [钉钉](https://qr.dingtalk.com/action/joingroup?code=v1,k1,7qkY1oyphgJvdUE4nJ1EcnNvE2JhmoNXBgdVTvD3AX0=&_dt_no_comment=1&origin=11)(主要通过中文来交流,群号 27855025593) 66 | * [Slack](https://join.slack.com/t/traas-kapacity/shared_invite/zt-1w1esmmk5-bNy3~IuGeCWQ21UmCexcrA)(主要通过英文来交流) 67 | 68 | ## 贡献 69 | 70 | 我们热忱地欢迎任何形式的贡献 🤗,你可以阅读[贡献指南](https://kapacity.netlify.app/zh-cn/docs/contribution-guidelines/)来了解更多贡献相关信息。 71 | -------------------------------------------------------------------------------- /pkg/scale/scaler.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | Copyright 2015 The Kubernetes Authors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package scale 19 | 20 | import ( 21 | "context" 22 | "fmt" 23 | 24 | k8sautoscalingv1 "k8s.io/api/autoscaling/v1" 25 | k8sautoscalingv2 "k8s.io/api/autoscaling/v2" 26 | apimeta "k8s.io/apimachinery/pkg/api/meta" 27 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 | "k8s.io/apimachinery/pkg/runtime/schema" 29 | scaleclient "k8s.io/client-go/scale" 30 | ) 31 | 32 | // Scaler is a convenient wrapper of ScalesGetter which provides a helper method 33 | // to get the scale of any scale target. 34 | type Scaler struct { 35 | scaleclient.ScalesGetter 36 | restMapper apimeta.RESTMapper 37 | } 38 | 39 | // NewScaler creates a new Scaler with the given scales getter and rest mapper. 40 | func NewScaler(scalesGetter scaleclient.ScalesGetter, restMapper apimeta.RESTMapper) *Scaler { 41 | return &Scaler{ 42 | ScalesGetter: scalesGetter, 43 | restMapper: restMapper, 44 | } 45 | } 46 | 47 | // GetScale return the scale as well as the group-resource of the specified scale target. 48 | func (s *Scaler) GetScale(ctx context.Context, namespace string, scaleTargetRef k8sautoscalingv2.CrossVersionObjectReference) (*k8sautoscalingv1.Scale, schema.GroupResource, error) { 49 | targetGV, err := schema.ParseGroupVersion(scaleTargetRef.APIVersion) 50 | if err != nil { 51 | return nil, schema.GroupResource{}, fmt.Errorf("invalid API version in scale target reference: %v", err) 52 | } 53 | targetGK := schema.GroupKind{ 54 | Group: targetGV.Group, 55 | Kind: scaleTargetRef.Kind, 56 | } 57 | mappings, err := s.restMapper.RESTMappings(targetGK) 58 | if err != nil { 59 | return nil, schema.GroupResource{}, fmt.Errorf("unable to determine resource for scale target reference: %v", err) 60 | } 61 | scale, targetGR, err := s.scaleForResourceMappings(ctx, namespace, scaleTargetRef.Name, mappings) 62 | if err != nil { 63 | return nil, schema.GroupResource{}, fmt.Errorf("failed to query scale subresource: %v", err) 64 | } 65 | return scale, targetGR, nil 66 | } 67 | 68 | // scaleForResourceMappings attempts to fetch the scale for the resource with the given name and namespace, 69 | // trying each RESTMapping in turn until a working one is found. 70 | // If none work, the first error is returned. 71 | // It returns both the scale, as well as the group-resource from the working mapping. 72 | func (s *Scaler) scaleForResourceMappings(ctx context.Context, namespace, name string, mappings []*apimeta.RESTMapping) (*k8sautoscalingv1.Scale, schema.GroupResource, error) { 73 | var firstErr error 74 | for i, mapping := range mappings { 75 | targetGR := mapping.Resource.GroupResource() 76 | scale, err := s.Scales(namespace).Get(ctx, targetGR, name, metav1.GetOptions{}) 77 | if err == nil { 78 | return scale, targetGR, nil 79 | } 80 | // if this is the first error, remember it, 81 | // then go on and try other mappings until we find a good one 82 | if i == 0 { 83 | firstErr = err 84 | } 85 | } 86 | // make sure we handle an empty set of mappings 87 | if firstErr == nil { 88 | firstErr = fmt.Errorf("unrecognized resource") 89 | } 90 | return nil, schema.GroupResource{}, firstErr 91 | } 92 | -------------------------------------------------------------------------------- /algorithm/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | .idea/ 161 | 162 | # macOS paraphernalia 163 | .DS_Store 164 | -------------------------------------------------------------------------------- /pkg/util/wait_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | Copyright 2014 The Kubernetes Authors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package util 19 | 20 | import ( 21 | "context" 22 | "errors" 23 | "math" 24 | "testing" 25 | "time" 26 | 27 | "k8s.io/apimachinery/pkg/util/wait" 28 | ) 29 | 30 | func TestExponentialBackoffWithContext(t *testing.T) { 31 | defaultCtx := func() (context.Context, context.CancelFunc) { 32 | return context.WithCancel(context.Background()) 33 | } 34 | 35 | defaultCallback := func(_ int) (bool, error) { 36 | return false, nil 37 | } 38 | 39 | conditionErr := errors.New("condition failed") 40 | 41 | tests := []struct { 42 | name string 43 | cap time.Duration 44 | ctxGetter func() (context.Context, context.CancelFunc) 45 | callback func(calls int) (bool, error) 46 | attemptsExpected int 47 | errExpected error 48 | }{ 49 | { 50 | name: "condition returns true with single backoff step", 51 | ctxGetter: defaultCtx, 52 | callback: func(_ int) (bool, error) { 53 | return true, nil 54 | }, 55 | attemptsExpected: 1, 56 | errExpected: nil, 57 | }, 58 | { 59 | name: "condition always returns false with multiple backoff steps", 60 | ctxGetter: defaultCtx, 61 | callback: defaultCallback, 62 | attemptsExpected: 5, 63 | errExpected: nil, 64 | }, 65 | { 66 | name: "condition returns true after certain attempts with multiple backoff steps", 67 | ctxGetter: defaultCtx, 68 | callback: func(attempts int) (bool, error) { 69 | if attempts == 3 { 70 | return true, nil 71 | } 72 | return false, nil 73 | }, 74 | attemptsExpected: 3, 75 | errExpected: nil, 76 | }, 77 | { 78 | name: "condition returns error no further attempts expected", 79 | ctxGetter: defaultCtx, 80 | callback: func(_ int) (bool, error) { 81 | return true, conditionErr 82 | }, 83 | attemptsExpected: 1, 84 | errExpected: conditionErr, 85 | }, 86 | { 87 | name: "context already canceled no attempts expected", 88 | ctxGetter: func() (context.Context, context.CancelFunc) { 89 | ctx, cancel := context.WithCancel(context.Background()) 90 | defer cancel() 91 | return ctx, cancel 92 | }, 93 | callback: defaultCallback, 94 | attemptsExpected: 0, 95 | errExpected: nil, 96 | }, 97 | } 98 | 99 | for _, test := range tests { 100 | t.Run(test.name, func(t *testing.T) { 101 | backoff := wait.Backoff{ 102 | Duration: 10 * time.Millisecond, 103 | Factor: 2, 104 | Steps: math.MaxInt32, 105 | Cap: 40 * time.Millisecond, 106 | } 107 | 108 | ctx, cancel := test.ctxGetter() 109 | 110 | go func() { 111 | time.Sleep(120 * time.Millisecond) 112 | cancel() 113 | }() 114 | 115 | attempts := 0 116 | err := ExponentialBackoffWithContext(ctx, backoff, func(context.Context) (bool, error) { 117 | attempts++ 118 | return test.callback(attempts) 119 | }) 120 | 121 | if !errors.Is(err, test.errExpected) { 122 | t.Errorf("expected error: %v but got: %v", test.errExpected, err) 123 | } 124 | 125 | if test.attemptsExpected != attempts { 126 | t.Errorf("expected attempts count: %d but got: %d", test.attemptsExpected, attempts) 127 | } 128 | }) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /pkg/pod/traffic/readinessgate_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package traffic 18 | 19 | import ( 20 | "context" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | corev1 "k8s.io/api/core/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/labels" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 29 | ) 30 | 31 | func TestOn(t *testing.T) { 32 | fakeClient := fake.NewClientBuilder().WithObjects().Build() 33 | readinessGate := ReadinessGate{Client: fakeClient} 34 | 35 | pods := []*corev1.Pod{ 36 | { 37 | ObjectMeta: metav1.ObjectMeta{ 38 | Name: "pod-1", 39 | Labels: map[string]string{ 40 | "foo": "bar", 41 | }, 42 | }, 43 | Spec: corev1.PodSpec{}, 44 | Status: corev1.PodStatus{}, 45 | }, 46 | } 47 | // create pod 48 | for _, pod := range pods { 49 | assert.Nil(t, fakeClient.Create(context.Background(), pod)) 50 | } 51 | defer cleanPods(fakeClient, pods) 52 | 53 | err := readinessGate.On(context.Background(), pods) 54 | assert.Nil(t, err) 55 | 56 | podList := &corev1.PodList{} 57 | ls, _ := labels.Parse("foo=bar") 58 | assert.Nil(t, fakeClient.List(context.Background(), podList, &client.ListOptions{LabelSelector: ls})) 59 | 60 | for _, pod := range podList.Items { 61 | assert.NotNil(t, pod.Status, "pod status should not nil for %s", pod.Name) 62 | assert.True(t, len(pod.Status.Conditions) > 0, "conditions should not empty for %s", pod.Name) 63 | assert.True(t, hasExpectedTraffic(pod.Status.Conditions, corev1.ConditionTrue), "pod traffic not on for %s", pod.Name) 64 | } 65 | } 66 | 67 | func TestOff(t *testing.T) { 68 | fakeClient := fake.NewClientBuilder().WithObjects().Build() 69 | readinessGate := ReadinessGate{Client: fakeClient} 70 | 71 | pods := []*corev1.Pod{ 72 | { 73 | ObjectMeta: metav1.ObjectMeta{ 74 | Name: "pod-2", 75 | Labels: map[string]string{ 76 | "foo": "bar", 77 | }, 78 | }, 79 | Spec: corev1.PodSpec{}, 80 | Status: corev1.PodStatus{}, 81 | }, 82 | } 83 | 84 | // create pod 85 | for _, pod := range pods { 86 | assert.Nil(t, fakeClient.Create(context.Background(), pod)) 87 | } 88 | defer cleanPods(fakeClient, pods) 89 | 90 | err := readinessGate.Off(context.Background(), pods) 91 | assert.Nil(t, err) 92 | 93 | podList := &corev1.PodList{} 94 | ls, _ := labels.Parse("foo=bar") 95 | assert.Nil(t, fakeClient.List(context.Background(), podList, &client.ListOptions{LabelSelector: ls})) 96 | 97 | for _, pod := range podList.Items { 98 | assert.NotNil(t, pod.Status, "pod status should not nil for %s", pod.Name) 99 | assert.True(t, len(pod.Status.Conditions) > 0, "conditions should not empty for %s", pod.Name) 100 | assert.True(t, hasExpectedTraffic(pod.Status.Conditions, corev1.ConditionFalse), "pod traffic not off for %s", pod.Name) 101 | } 102 | } 103 | 104 | func hasExpectedTraffic(conditions []corev1.PodCondition, expectedStatus corev1.ConditionStatus) bool { 105 | for _, condition := range conditions { 106 | if condition.Type == ReadinessGateOnline { 107 | return condition.Status == expectedStatus 108 | } 109 | } 110 | 111 | return false 112 | } 113 | 114 | func cleanPods(c client.Client, pods []*corev1.Pod) { 115 | for _, pod := range pods { 116 | _ = c.Delete(context.Background(), pod) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /pkg/metric/provider/prometheus/metrics_config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package prometheus 18 | 19 | import ( 20 | "fmt" 21 | "io" 22 | "os" 23 | 24 | prommodel "github.com/prometheus/common/model" 25 | "gopkg.in/yaml.v2" 26 | promadaptercfg "sigs.k8s.io/prometheus-adapter/pkg/config" 27 | ) 28 | 29 | // MetricsDiscoveryConfig is an extension of promadaptercfg.MetricsDiscoveryConfig 30 | // which includes extra configurations for advanced metrics queries used by Kapacity. 31 | type MetricsDiscoveryConfig struct { 32 | ResourceRules *ResourceRules `json:"resourceRules,omitempty" yaml:"resourceRules,omitempty"` 33 | Rules []promadaptercfg.DiscoveryRule `json:"rules,omitempty" yaml:"rules,omitempty"` 34 | ExternalRules []promadaptercfg.DiscoveryRule `json:"externalRules,omitempty" yaml:"externalRules,omitempty"` 35 | WorkloadPodNamePatterns []WorkloadPodNamePattern `json:"workloadPodNamePatterns,omitempty" yaml:"workloadPodNamePatterns,omitempty"` 36 | } 37 | 38 | // ResourceRules is an extension of promadaptercfg.ResourceRules 39 | // which includes extra configurations for advanced metrics queries used by Kapacity. 40 | type ResourceRules struct { 41 | CPU ResourceRule `json:"cpu" yaml:"cpu"` 42 | Memory ResourceRule `json:"memory" yaml:"memory"` 43 | Window prommodel.Duration `json:"window" yaml:"window"` 44 | } 45 | 46 | // ResourceRule is an extension of promadaptercfg.ResourceRule 47 | // which includes extra configurations for advanced metrics queries used by Kapacity. 48 | type ResourceRule struct { 49 | promadaptercfg.ResourceRule `json:",inline" yaml:",inline"` 50 | // ReadyPodsOnlyContainerQuery is the query used to fetch the metrics for containers of ready Pods only. 51 | ReadyPodsOnlyContainerQuery string `json:"readyPodsOnlyContainerQuery" yaml:"readyPodsOnlyContainerQuery"` 52 | } 53 | 54 | // WorkloadPodNamePattern describes the pod name pattern of a specific kind of workload. 55 | type WorkloadPodNamePattern struct { 56 | // GroupKind is the group-kind of the workload. 57 | GroupKind `json:",inline" yaml:",inline"` 58 | // Pattern is a regex expression which matches all the pods belonging to a specific workload. 59 | // The workload's name placeholder should be "%s" and would be replaced by the name 60 | // of a specific workload during runtime. 61 | Pattern string `json:"pattern" yaml:"pattern"` 62 | } 63 | 64 | // GroupKind represents a Kubernetes group-kind. 65 | type GroupKind struct { 66 | Group string `json:"group,omitempty" yaml:"group,omitempty"` 67 | Kind string `json:"kind" yaml:"kind"` 68 | } 69 | 70 | // MetricsConfigFromFile loads MetricsDiscoveryConfig from a particular file. 71 | func MetricsConfigFromFile(filename string) (*MetricsDiscoveryConfig, error) { 72 | file, err := os.Open(filename) 73 | if err != nil { 74 | return nil, fmt.Errorf("unable to load metrics discovery config file: %v", err) 75 | } 76 | defer file.Close() 77 | contents, err := io.ReadAll(file) 78 | if err != nil { 79 | return nil, fmt.Errorf("unable to load metrics discovery config file: %v", err) 80 | } 81 | return MetricsConfigFromYAML(contents) 82 | } 83 | 84 | // MetricsConfigFromYAML loads MetricsDiscoveryConfig from a blob of YAML. 85 | func MetricsConfigFromYAML(contents []byte) (*MetricsDiscoveryConfig, error) { 86 | var cfg MetricsDiscoveryConfig 87 | if err := yaml.UnmarshalStrict(contents, &cfg); err != nil { 88 | return nil, fmt.Errorf("unable to parse metrics discovery config: %v", err) 89 | } 90 | return &cfg, nil 91 | } 92 | -------------------------------------------------------------------------------- /pkg/metric/provider/prometheus/object_series_registry.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | Copyright 2017 The Kubernetes Authors. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | package prometheus 19 | 20 | import ( 21 | "fmt" 22 | "sync" 23 | 24 | apimeta "k8s.io/apimachinery/pkg/api/meta" 25 | "k8s.io/apimachinery/pkg/labels" 26 | cmaprovider "sigs.k8s.io/custom-metrics-apiserver/pkg/provider" 27 | promadapterclient "sigs.k8s.io/prometheus-adapter/pkg/client" 28 | "sigs.k8s.io/prometheus-adapter/pkg/naming" 29 | ) 30 | 31 | // objectSeriesRegistry acts as a low-level converter for transforming Kubernetes object metrics queries 32 | // into Prometheus queries. 33 | type objectSeriesRegistry struct { 34 | Mapper apimeta.RESTMapper 35 | 36 | mu sync.RWMutex 37 | // info maps metric info to information about the corresponding series 38 | info map[cmaprovider.CustomMetricInfo]seriesInfo 39 | } 40 | 41 | type seriesInfo struct { 42 | // SeriesName is the name of the corresponding Prometheus series. 43 | SeriesName string 44 | 45 | // Namer is the naming.MetricNamer used to name this series. 46 | Namer naming.MetricNamer 47 | } 48 | 49 | func (r *objectSeriesRegistry) QueryForMetric(metricInfo cmaprovider.CustomMetricInfo, namespace string, metricSelector labels.Selector, resourceNames ...string) (string, error) { 50 | r.mu.RLock() 51 | defer r.mu.RUnlock() 52 | 53 | if len(resourceNames) == 0 { 54 | return "", fmt.Errorf("no resource names requested while producing a query for metric %v", metricInfo) 55 | } 56 | 57 | metricInfo, _, err := metricInfo.Normalized(r.Mapper) 58 | if err != nil { 59 | return "", fmt.Errorf("unable to normalize group resource while producing a query: %v", err) 60 | } 61 | 62 | info, infoFound := r.info[metricInfo] 63 | if !infoFound { 64 | return "", fmt.Errorf("metric %v not registered", metricInfo) 65 | } 66 | 67 | query, err := info.Namer.QueryForSeries(info.SeriesName, metricInfo.GroupResource, namespace, metricSelector, resourceNames...) 68 | return string(query), err 69 | } 70 | 71 | // SetSeries replaces the known series in registry. 72 | // Each slice in series should correspond to a naming.MetricNamer in namers. 73 | func (r *objectSeriesRegistry) SetSeries(newSeriesSlices [][]promadapterclient.Series, namers []naming.MetricNamer) error { 74 | if len(newSeriesSlices) != len(namers) { 75 | return fmt.Errorf("need one set of series per namer") 76 | } 77 | 78 | newInfo := make(map[cmaprovider.CustomMetricInfo]seriesInfo) 79 | for i, newSeries := range newSeriesSlices { 80 | namer := namers[i] 81 | for _, series := range newSeries { 82 | resources, namespaced := namer.ResourcesForSeries(series) 83 | name, err := namer.MetricNameForSeries(series) 84 | if err != nil { 85 | metricsListerLog.Error(err, "unable to name series, skipping", "series", series.String()) 86 | continue 87 | } 88 | for _, resource := range resources { 89 | info := cmaprovider.CustomMetricInfo{ 90 | GroupResource: resource, 91 | Namespaced: namespaced, 92 | Metric: name, 93 | } 94 | 95 | // some metrics aren't counted as namespaced 96 | if resource == naming.NsGroupResource || resource == naming.NodeGroupResource || resource == naming.PVGroupResource { 97 | info.Namespaced = false 98 | } 99 | 100 | // we don't need to re-normalize, because the metric namer should have already normalized for us 101 | newInfo[info] = seriesInfo{ 102 | SeriesName: series.Name, 103 | Namer: namer, 104 | } 105 | } 106 | } 107 | } 108 | 109 | r.mu.Lock() 110 | defer r.mu.Unlock() 111 | r.info = newInfo 112 | 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /pkg/metric/query.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package metric 18 | 19 | import ( 20 | k8sautoscalingv2 "k8s.io/api/autoscaling/v2" 21 | corev1 "k8s.io/api/core/v1" 22 | "k8s.io/apimachinery/pkg/labels" 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | ) 25 | 26 | type QueryType string 27 | 28 | const ( 29 | // PodResourceQueryType is for resource metrics (such as those specified in requests and limits, e.g. CPU or memory) 30 | // known to Kubernetes describing each pod. 31 | PodResourceQueryType QueryType = "PodResource" 32 | // ContainerResourceQueryType is for resource metrics (such as those specified in requests and limits, e.g. CPU or memory) 33 | // known to Kubernetes describing a specific container in each pod. 34 | ContainerResourceQueryType QueryType = "ContainerResource" 35 | // WorkloadResourceQueryType is for resource metrics (such as those specified in requests and limits, e.g. CPU or memory) 36 | // known to Kubernetes describing each group of pods belonging to the same workload. 37 | WorkloadResourceQueryType QueryType = "WorkloadResource" 38 | // WorkloadContainerResourceQueryType is for resource metrics (such as those specified in requests and limits, e.g. CPU or memory) 39 | // known to Kubernetes describing a specific container in each group of pods belonging to the same workload. 40 | WorkloadContainerResourceQueryType QueryType = "WorkloadContainerResource" 41 | // ObjectQueryType is for metrics describing a single Kubernetes object 42 | // (e.g. hits-per-second on an Ingress object). 43 | ObjectQueryType QueryType = "Object" 44 | // ExternalQueryType is for global metrics that are not associated with any Kubernetes object 45 | // (e.g. length of queue in cloud messaging service or QPS from loadbalancer running outside of cluster). 46 | ExternalQueryType QueryType = "External" 47 | // WorkloadExternalQueryType is for global metrics describing each group of pods belonging to the same workload 48 | // (e.g. the total number of ready pods). 49 | WorkloadExternalQueryType QueryType = "WorkloadExternal" 50 | ) 51 | 52 | // Query represents a query for a specific type of metrics. 53 | type Query struct { 54 | Type QueryType 55 | PodResource *PodResourceQuery 56 | ContainerResource *ContainerResourceQuery 57 | WorkloadResource *WorkloadResourceQuery 58 | WorkloadContainerResource *WorkloadContainerResourceQuery 59 | Object *ObjectQuery 60 | External *ExternalQuery 61 | WorkloadExternal *WorkloadExternalQuery 62 | } 63 | 64 | type PodResourceQuery struct { 65 | Namespace string 66 | Name string 67 | Selector labels.Selector 68 | ResourceName corev1.ResourceName 69 | } 70 | 71 | type ContainerResourceQuery struct { 72 | PodResourceQuery 73 | ContainerName string 74 | } 75 | 76 | type WorkloadResourceQuery struct { 77 | GroupKind schema.GroupKind 78 | Namespace string 79 | Name string 80 | ResourceName corev1.ResourceName 81 | ReadyPodsOnly bool 82 | } 83 | 84 | type WorkloadContainerResourceQuery struct { 85 | WorkloadResourceQuery 86 | ContainerName string 87 | } 88 | 89 | type ObjectQuery struct { 90 | GroupKind schema.GroupKind 91 | Namespace string 92 | Name string 93 | Selector labels.Selector 94 | Metric k8sautoscalingv2.MetricIdentifier 95 | } 96 | 97 | type ExternalQuery struct { 98 | Namespace string 99 | Metric k8sautoscalingv2.MetricIdentifier 100 | } 101 | 102 | type WorkloadExternalQuery struct { 103 | GroupKind schema.GroupKind 104 | Namespace string 105 | Name string 106 | Metric k8sautoscalingv2.MetricIdentifier 107 | } 108 | -------------------------------------------------------------------------------- /algorithm/kapacity/metric/pb/provider_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | """Client and server classes corresponding to protobuf-defined services.""" 3 | import grpc 4 | 5 | from . import provider_pb2 as provider__pb2 6 | 7 | 8 | class ProviderServiceStub(object): 9 | """Missing associated documentation comment in .proto file.""" 10 | 11 | def __init__(self, channel): 12 | """Constructor. 13 | 14 | Args: 15 | channel: A grpc.Channel. 16 | """ 17 | self.QueryLatest = channel.unary_unary( 18 | '/io.kapacitystack.metric.ProviderService/QueryLatest', 19 | request_serializer=provider__pb2.QueryLatestRequest.SerializeToString, 20 | response_deserializer=provider__pb2.QueryLatestResponse.FromString, 21 | ) 22 | self.Query = channel.unary_unary( 23 | '/io.kapacitystack.metric.ProviderService/Query', 24 | request_serializer=provider__pb2.QueryRequest.SerializeToString, 25 | response_deserializer=provider__pb2.QueryResponse.FromString, 26 | ) 27 | 28 | 29 | class ProviderServiceServicer(object): 30 | """Missing associated documentation comment in .proto file.""" 31 | 32 | def QueryLatest(self, request, context): 33 | """Missing associated documentation comment in .proto file.""" 34 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 35 | context.set_details('Method not implemented!') 36 | raise NotImplementedError('Method not implemented!') 37 | 38 | def Query(self, request, context): 39 | """Missing associated documentation comment in .proto file.""" 40 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 41 | context.set_details('Method not implemented!') 42 | raise NotImplementedError('Method not implemented!') 43 | 44 | 45 | def add_ProviderServiceServicer_to_server(servicer, server): 46 | rpc_method_handlers = { 47 | 'QueryLatest': grpc.unary_unary_rpc_method_handler( 48 | servicer.QueryLatest, 49 | request_deserializer=provider__pb2.QueryLatestRequest.FromString, 50 | response_serializer=provider__pb2.QueryLatestResponse.SerializeToString, 51 | ), 52 | 'Query': grpc.unary_unary_rpc_method_handler( 53 | servicer.Query, 54 | request_deserializer=provider__pb2.QueryRequest.FromString, 55 | response_serializer=provider__pb2.QueryResponse.SerializeToString, 56 | ), 57 | } 58 | generic_handler = grpc.method_handlers_generic_handler( 59 | 'io.kapacitystack.metric.ProviderService', rpc_method_handlers) 60 | server.add_generic_rpc_handlers((generic_handler,)) 61 | 62 | 63 | # This class is part of an EXPERIMENTAL API. 64 | class ProviderService(object): 65 | """Missing associated documentation comment in .proto file.""" 66 | 67 | @staticmethod 68 | def QueryLatest(request, 69 | target, 70 | options=(), 71 | channel_credentials=None, 72 | call_credentials=None, 73 | insecure=False, 74 | compression=None, 75 | wait_for_ready=None, 76 | timeout=None, 77 | metadata=None): 78 | return grpc.experimental.unary_unary(request, target, '/io.kapacitystack.metric.ProviderService/QueryLatest', 79 | provider__pb2.QueryLatestRequest.SerializeToString, 80 | provider__pb2.QueryLatestResponse.FromString, 81 | options, channel_credentials, 82 | insecure, call_credentials, compression, wait_for_ready, timeout, metadata) 83 | 84 | @staticmethod 85 | def Query(request, 86 | target, 87 | options=(), 88 | channel_credentials=None, 89 | call_credentials=None, 90 | insecure=False, 91 | compression=None, 92 | wait_for_ready=None, 93 | timeout=None, 94 | metadata=None): 95 | return grpc.experimental.unary_unary(request, target, '/io.kapacitystack.metric.ProviderService/Query', 96 | provider__pb2.QueryRequest.SerializeToString, 97 | provider__pb2.QueryResponse.FromString, 98 | options, channel_credentials, 99 | insecure, call_credentials, compression, wait_for_ready, timeout, metadata) 100 | -------------------------------------------------------------------------------- /algorithm/kapacity/timeseries/forecasting/train.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 The Kapacity Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import argparse 17 | import kapacity.metric.query as query 18 | import kapacity.timeseries.forecasting.forecaster as forecaster 19 | import pandas as pd 20 | import yaml 21 | 22 | 23 | def main(): 24 | # 1. parse arguments 25 | args = parse_args() 26 | 27 | # 2. load training config 28 | config = load_config(args.config_file) 29 | 30 | # 3. fetch training dataset 31 | df = fetch_train_data(args, config) 32 | 33 | # 4. train the model 34 | train(args, config, df) 35 | 36 | return 37 | 38 | 39 | def parse_args(): 40 | parser = argparse.ArgumentParser(description='args of timeseries forcasting training') 41 | parser.add_argument('--config-file', help='path of training config file', required=True) 42 | parser.add_argument('--model-save-path', help='dir path where the model and its related files would be saved in', 43 | required=True) 44 | parser.add_argument('--dataset-file', help='path of training dataset file, if set, will load dataset from this ' 45 | 'file instead of fetching from metrics server', required=False) 46 | parser.add_argument('--metrics-server-addr', help='address of the gRPC metrics provider server', required=False) 47 | parser.add_argument('--dataloader-num-workers', help='number of worker subprocesses to use for data loading', 48 | required=False, default=0) 49 | 50 | args = parser.parse_args() 51 | return args 52 | 53 | 54 | def train(args, config, df): 55 | return forecaster.fit( 56 | df=df, 57 | freq=config['freq'], 58 | target_col='value', 59 | time_col='timestamp', 60 | series_cols=['workload', 'metric'], 61 | prediction_length=config['predictionLength'], 62 | context_length=config['contextLength'], 63 | learning_rate=config['hyperParams']['learningRate'], 64 | epochs=config['hyperParams']['epochs'], 65 | batch_size=config['hyperParams']['batchSize'], 66 | num_workers=int(args.dataloader_num_workers), 67 | model_path=args.model_save_path) 68 | 69 | 70 | def load_config(config_file): 71 | with open(config_file, 'r') as f: 72 | return yaml.safe_load(f) 73 | 74 | 75 | def fetch_train_data(args, config): 76 | if args.dataset_file is not None: 77 | df = pd.read_csv(args.dataset_file) 78 | else: 79 | df = pd.DataFrame(columns=['timestamp', 'value', 'workload', 'metric']) 80 | for i in range(len(config['targets'])): 81 | target = config['targets'][i] 82 | df_target = fetch_history_metrics(args, target) 83 | df = pd.concat([df, df_target]) 84 | return df 85 | 86 | 87 | def fetch_history_metrics(args, target): 88 | workload_namespace = target['workloadNamespace'] 89 | workload_name = target['workloadRef']['name'] 90 | workload_identifier = '%s/%s' % (workload_namespace, workload_name) 91 | start, end = query.compute_history_range(target['historyLength']) 92 | 93 | df = pd.DataFrame(columns=['timestamp', 'value', 'workload', 'metric']) 94 | for i in range(len(target['metrics'])): 95 | metric = target['metrics'][i] 96 | df_metric = query.fetch_metrics(args.metrics_server_addr, 97 | workload_namespace, 98 | metric, 99 | target['workloadRef'], 100 | start, 101 | end) 102 | df_metric['metric'] = metric['name'] 103 | df_metric['workload'] = workload_identifier 104 | df = pd.concat([df, df_metric]) 105 | return df 106 | 107 | 108 | if __name__ == '__main__': 109 | main() 110 | -------------------------------------------------------------------------------- /pkg/portrait/algorithm/externaljob/jobcontroller/cronjob_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package jobcontroller 18 | 19 | import ( 20 | "context" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | batchv1 "k8s.io/api/batch/v1" 25 | apierrors "k8s.io/apimachinery/pkg/api/errors" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | "k8s.io/apimachinery/pkg/runtime" 28 | "k8s.io/apimachinery/pkg/types" 29 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 30 | 31 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 32 | 33 | autoscalingv1alpha1 "github.com/traas-stack/kapacity/apis/autoscaling/v1alpha1" 34 | ) 35 | 36 | const ( 37 | cronJobNamespace = "cron-job-test" 38 | 39 | hpNamespace = "test" 40 | hpName = "test-predictive" 41 | cronJobName = hpNamespace + "-" + hpName 42 | ) 43 | 44 | var ( 45 | scheme = runtime.NewScheme() 46 | ) 47 | 48 | func init() { 49 | _ = clientgoscheme.AddToScheme(scheme) 50 | _ = autoscalingv1alpha1.AddToScheme(scheme) 51 | } 52 | 53 | func TestCronJobHorizontal_Create(t *testing.T) { 54 | fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() 55 | ctx := context.Background() 56 | horizontalPortrait := &autoscalingv1alpha1.HorizontalPortrait{ 57 | ObjectMeta: metav1.ObjectMeta{ 58 | Namespace: hpNamespace, 59 | Name: hpName, 60 | }, 61 | } 62 | 63 | cfg := &autoscalingv1alpha1.PortraitAlgorithmJob{ 64 | Type: autoscalingv1alpha1.CronJobPortraitAlgorithmJobType, 65 | CronJob: &autoscalingv1alpha1.CronJobPortraitAlgorithmJob{ 66 | Template: autoscalingv1alpha1.CronJobTemplateSpec{ 67 | ObjectMeta: metav1.ObjectMeta{ 68 | Labels: map[string]string{"cronjob": "test"}, 69 | }, 70 | Spec: batchv1.CronJobSpec{ 71 | Schedule: "* * * * *", 72 | JobTemplate: batchv1.JobTemplateSpec{}, 73 | }, 74 | }, 75 | }, 76 | } 77 | 78 | cronJobHorizontal := NewCronJobHorizontal(fakeClient, cronJobNamespace, "", "", nil) 79 | err := cronJobHorizontal.UpdateJob(ctx, horizontalPortrait, cfg) 80 | assert.Nil(t, err) 81 | 82 | cronJob := &batchv1.CronJob{} 83 | _ = fakeClient.Get(ctx, types.NamespacedName{Namespace: cronJobNamespace, Name: cronJobName}, cronJob) 84 | assert.NotNil(t, cronJob) 85 | 86 | err = cronJobHorizontal.CleanupJob(ctx, horizontalPortrait) 87 | assert.Nil(t, err) 88 | 89 | err = fakeClient.Get(ctx, types.NamespacedName{Namespace: cronJobNamespace, Name: cronJobName}, cronJob) 90 | assert.True(t, apierrors.IsNotFound(err)) 91 | } 92 | 93 | func TestCronJobHorizontal_Update(t *testing.T) { 94 | fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() 95 | ctx := context.Background() 96 | horizontalPortrait := &autoscalingv1alpha1.HorizontalPortrait{ 97 | ObjectMeta: metav1.ObjectMeta{ 98 | Namespace: hpNamespace, 99 | Name: hpName, 100 | }, 101 | } 102 | 103 | cfg := &autoscalingv1alpha1.PortraitAlgorithmJob{ 104 | Type: autoscalingv1alpha1.CronJobPortraitAlgorithmJobType, 105 | CronJob: &autoscalingv1alpha1.CronJobPortraitAlgorithmJob{ 106 | Template: autoscalingv1alpha1.CronJobTemplateSpec{ 107 | ObjectMeta: metav1.ObjectMeta{ 108 | Labels: map[string]string{"cronjob": "test"}, 109 | Annotations: map[string]string{"a": "b"}, 110 | }, 111 | Spec: batchv1.CronJobSpec{ 112 | Schedule: "* * * * *", 113 | JobTemplate: batchv1.JobTemplateSpec{}, 114 | }, 115 | }, 116 | }, 117 | } 118 | 119 | cronJob := &batchv1.CronJob{ 120 | ObjectMeta: metav1.ObjectMeta{ 121 | Namespace: cronJobNamespace, 122 | Name: cronJobName, 123 | Annotations: map[string]string{"c": "d"}, 124 | }, 125 | Spec: batchv1.CronJobSpec{}, 126 | } 127 | _ = fakeClient.Create(ctx, cronJob) 128 | 129 | cronJobHorizontal := NewCronJobHorizontal(fakeClient, cronJobNamespace, "", "", nil) 130 | err := cronJobHorizontal.UpdateJob(ctx, horizontalPortrait, cfg) 131 | assert.Nil(t, err) 132 | 133 | _ = fakeClient.Get(ctx, types.NamespacedName{Namespace: cronJobNamespace, Name: cronJobName}, cronJob) 134 | assert.NotNil(t, cronJob) 135 | assert.True(t, len(cronJob.Labels) > 0) 136 | 137 | _ = cronJobHorizontal.CleanupJob(ctx, horizontalPortrait) 138 | } 139 | -------------------------------------------------------------------------------- /pkg/portrait/generator/reactive/metrics_client_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kapacity Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package reactive 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "testing" 23 | "time" 24 | 25 | "github.com/stretchr/testify/assert" 26 | corev1 "k8s.io/api/core/v1" 27 | "k8s.io/apimachinery/pkg/api/resource" 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | "k8s.io/apimachinery/pkg/labels" 30 | "k8s.io/apimachinery/pkg/runtime" 31 | coretesting "k8s.io/client-go/testing" 32 | "k8s.io/metrics/pkg/apis/metrics/v1beta1" 33 | metricsfake "k8s.io/metrics/pkg/client/clientset/versioned/fake" 34 | 35 | "github.com/traas-stack/kapacity/pkg/metric/provider/metricsapi" 36 | ) 37 | 38 | const ( 39 | testNamespace = "test-namespace" 40 | podNamePrefix = "test-pod" 41 | ) 42 | 43 | func TestGetResourceMetric_UnsupportedResource(t *testing.T) { 44 | metricsClient := metricsClient{} 45 | 46 | selector, _ := labels.Parse("foo=bar") 47 | _, _, err := metricsClient.GetResourceMetric(context.Background(), corev1.ResourceStorage, testNamespace, selector, "test-container") 48 | assert.NotNil(t, err, "unsupported resource for %s", corev1.ResourceStorage) 49 | } 50 | 51 | func TestGetResourceMetric(t *testing.T) { 52 | testCases := []*metricsTestCase{ 53 | { 54 | resourceName: corev1.ResourceCPU, 55 | podMetricsMap: map[string][]int64{ 56 | fmt.Sprintf("%s-%d", podNamePrefix, 1): {300, 500, 700}, 57 | }, 58 | timestamp: time.Now(), 59 | }, 60 | } 61 | 62 | for _, testCase := range testCases { 63 | fakeMetricsClient := prepareFakeMetricsClient(testCase.resourceName, testCase.podMetricsMap, testCase.timestamp) 64 | metricsClient := metricsClient{ 65 | metricProvider: metricsapi.NewMetricProvider( 66 | fakeMetricsClient.MetricsV1beta1(), 67 | ), 68 | } 69 | 70 | selector, _ := labels.Parse("name=test-pod") 71 | // pod resources 72 | podMetrics, _, err := metricsClient.GetResourceMetric(context.Background(), testCase.resourceName, testNamespace, selector, "") 73 | assert.Nil(t, err) 74 | 75 | for podName, resValues := range testCase.podMetricsMap { 76 | var expectedValue int64 = 0 77 | for index, containerValue := range resValues { 78 | expectedValue += containerValue 79 | 80 | // container resources 81 | containerName := buildContainerName(podName, index+1) 82 | containerMetrics, _, err := metricsClient.GetResourceMetric(context.Background(), testCase.resourceName, testNamespace, selector, containerName) 83 | assert.Nil(t, err, "failed to get resource metrics") 84 | assert.NotNil(t, containerMetrics, "container metrics not found for %s", containerName) 85 | assert.Equal(t, containerValue, containerMetrics[podName].Value, "container metrics not expected for %s", containerName) 86 | } 87 | 88 | assert.Equal(t, expectedValue, podMetrics[podName].Value, "pod metrics not expected for %s", podName) 89 | } 90 | } 91 | } 92 | 93 | type metricsTestCase struct { 94 | resourceName corev1.ResourceName 95 | // key is pod name, value is container resource metric values 96 | podMetricsMap map[string][]int64 97 | timestamp time.Time 98 | } 99 | 100 | func prepareFakeMetricsClient(resourceName corev1.ResourceName, podMetricsMap map[string][]int64, timestamp time.Time) *metricsfake.Clientset { 101 | fakeMetricsClient := &metricsfake.Clientset{} 102 | fakeMetricsClient.AddReactor("list", "pods", func(action coretesting.Action) (handled bool, ret runtime.Object, err error) { 103 | metrics := &v1beta1.PodMetricsList{} 104 | for podName, resValue := range podMetricsMap { 105 | podMetric := v1beta1.PodMetrics{ 106 | ObjectMeta: metav1.ObjectMeta{ 107 | Name: podName, 108 | Namespace: testNamespace, 109 | Labels: map[string]string{"name": podNamePrefix}, 110 | }, 111 | Timestamp: metav1.Time{Time: timestamp}, 112 | Window: metav1.Duration{Duration: time.Minute}, 113 | Containers: make([]v1beta1.ContainerMetrics, len(resValue)), 114 | } 115 | 116 | for i, m := range resValue { 117 | podMetric.Containers[i] = v1beta1.ContainerMetrics{ 118 | Name: buildContainerName(podName, i+1), 119 | Usage: corev1.ResourceList{ 120 | resourceName: *resource.NewMilliQuantity(m, resource.DecimalSI), 121 | }, 122 | } 123 | } 124 | metrics.Items = append(metrics.Items, podMetric) 125 | } 126 | return true, metrics, nil 127 | }) 128 | return fakeMetricsClient 129 | } 130 | 131 | func buildContainerName(prefix string, index int) string { 132 | return fmt.Sprintf("%s-container-%v", prefix, index) 133 | } 134 | 135 | func buildPodName(index int) string { 136 | return fmt.Sprintf("%s-%v", podNamePrefix, index) 137 | } 138 | --------------------------------------------------------------------------------