├── 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 |
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 |
5 |
6 |
7 |
8 | [](https://pkg.go.dev/github.com/traas-stack/kapacity)
9 | [](https://www.apache.org/licenses/LICENSE-2.0.html)
10 | 
11 | [](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 |
--------------------------------------------------------------------------------