infra: add stateless gitea runner scaler
This commit is contained in:
parent
04688742b3
commit
2e1266cede
|
|
@ -0,0 +1,22 @@
|
|||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: runner-scaler
|
||||
namespace: argocd
|
||||
finalizers:
|
||||
- resources-finalizer.argocd.argoproj.io
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: 'git@bitbucket.org:jamkazam/video-iac.git'
|
||||
targetRevision: develop
|
||||
path: k8s/jam-cloud-infra
|
||||
directory:
|
||||
include: 'runner-scaler.yaml'
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: jam-cloud-infra
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: gitea-external-scaler-secret
|
||||
namespace: jam-cloud-infra
|
||||
type: Opaque
|
||||
stringData:
|
||||
webhookSecret: f5edc33f5a9ea1b01c6a75883f2f899b526c860d0f5654ae21faa4c1a61c43d5
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: gitea-external-scaler
|
||||
namespace: jam-cloud-infra
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: gitea-external-scaler
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: gitea-external-scaler
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: gitea-registry
|
||||
containers:
|
||||
- name: scaler
|
||||
image: git.staging.jamkazam.com/seth/gitea-external-scaler:2.0.1
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- name: grpc
|
||||
containerPort: 50051
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
env:
|
||||
- name: REDIS_ADDR
|
||||
value: redis.jam-cloud-infra.svc.cluster.local:6379
|
||||
- name: REDIS_DB
|
||||
value: "1"
|
||||
- name: WEBHOOK_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: gitea-external-scaler-secret
|
||||
key: webhookSecret
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
restartPolicy: Always
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: gitea-external-scaler
|
||||
namespace: jam-cloud-infra
|
||||
spec:
|
||||
selector:
|
||||
app: gitea-external-scaler
|
||||
ports:
|
||||
- name: grpc
|
||||
port: 50051
|
||||
targetPort: grpc
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: http
|
||||
---
|
||||
apiVersion: keda.sh/v1alpha1
|
||||
kind: ScaledObject
|
||||
metadata:
|
||||
name: act-runner-scaler
|
||||
namespace: jam-cloud-infra
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
name: act-runner
|
||||
minReplicaCount: 0
|
||||
maxReplicaCount: 5
|
||||
cooldownPeriod: 300
|
||||
pollingInterval: 15
|
||||
triggers:
|
||||
- type: external
|
||||
metadata:
|
||||
scalerAddress: gitea-external-scaler.jam-cloud-infra.svc.cluster.local:50051
|
||||
labels: ubuntu-latest
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache protobuf
|
||||
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.10
|
||||
RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN mkdir -p externalscaler
|
||||
RUN protoc --go_out=externalscaler --go_opt=paths=source_relative \
|
||||
--go-grpc_out=externalscaler --go-grpc_opt=paths=source_relative \
|
||||
externalscaler.proto
|
||||
|
||||
RUN go mod tidy
|
||||
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} go build -o scaler main.go
|
||||
|
||||
FROM alpine:3.22
|
||||
WORKDIR /root/
|
||||
COPY --from=builder /app/scaler .
|
||||
EXPOSE 50051 8080
|
||||
CMD ["./scaler"]
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
Stateless Gitea runner scaler.
|
||||
|
||||
Design:
|
||||
- Gitea emits `workflow_job` webhooks to `POST /webhooks/workflow-job`
|
||||
- This service verifies the webhook signature and tracks queued jobs in Redis
|
||||
- KEDA polls the gRPC external scaler API on port `50051`
|
||||
- The scale target is `Deployment/act-runner`
|
||||
|
||||
Why this exists:
|
||||
- The previous scaler read `gitea.db` from the `gitea-data` SQLite volume
|
||||
- That coupled scaler scheduling and failover to the same `ReadWriteOnce` PVC as Gitea
|
||||
- This redesign removes that PVC dependency entirely
|
||||
|
||||
Current assumptions:
|
||||
- Gitea webhook payloads are GitHub-compatible enough to provide:
|
||||
- `action`
|
||||
- `workflow_job.id`
|
||||
- `workflow_job.labels`
|
||||
- `workflow_job.status`
|
||||
- The label we currently care about is `ubuntu-latest`
|
||||
|
||||
Required wiring before production/staging rollout:
|
||||
1. Build and publish `git.staging.jamkazam.com/seth/gitea-external-scaler:2.0.1`
|
||||
2. Set a real `webhookSecret` in `runner-scaler.yaml`
|
||||
3. Add repo or instance webhooks in Gitea that send `workflow_job` events to:
|
||||
`http://gitea-external-scaler.jam-cloud-infra.svc.cluster.local:8080/webhooks/workflow-job`
|
||||
4. Decide whether `act-runner` desired replicas should move to `0` in repo-managed state
|
||||
|
||||
Reference:
|
||||
- Gitea docs recommend the `workflow_job` webhook to automate registration/startup of new runners.
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package externalscaler;
|
||||
option go_package = ".;externalscaler";
|
||||
|
||||
service ExternalScaler {
|
||||
rpc IsActive(ScaledObjectRef) returns (IsActiveResponse) {}
|
||||
rpc StreamIsActive(ScaledObjectRef) returns (stream IsActiveResponse) {}
|
||||
rpc GetMetricSpec(ScaledObjectRef) returns (GetMetricSpecResponse) {}
|
||||
rpc GetMetrics(GetMetricsRequest) returns (GetMetricsResponse) {}
|
||||
}
|
||||
|
||||
message ScaledObjectRef {
|
||||
string name = 1;
|
||||
string namespace = 2;
|
||||
map<string, string> scalerMetadata = 3;
|
||||
}
|
||||
|
||||
message IsActiveResponse {
|
||||
bool result = 1;
|
||||
}
|
||||
|
||||
message GetMetricSpecResponse {
|
||||
repeated MetricSpec metricSpecs = 1;
|
||||
}
|
||||
|
||||
message MetricSpec {
|
||||
string metricName = 1;
|
||||
int64 targetSize = 2;
|
||||
double targetSizeFloat = 3;
|
||||
}
|
||||
|
||||
message GetMetricsRequest {
|
||||
ScaledObjectRef scaledObjectRef = 1;
|
||||
string metricName = 2;
|
||||
}
|
||||
|
||||
message GetMetricsResponse {
|
||||
repeated MetricValue metricValues = 1;
|
||||
}
|
||||
|
||||
message MetricValue {
|
||||
string metricName = 1;
|
||||
int64 metricValue = 2;
|
||||
double metricValueFloat = 3;
|
||||
}
|
||||
|
|
@ -0,0 +1,498 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.10
|
||||
// protoc v6.33.4
|
||||
// source: externalscaler.proto
|
||||
|
||||
package externalscaler
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type ScaledObjectRef struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"`
|
||||
ScalerMetadata map[string]string `protobuf:"bytes,3,rep,name=scalerMetadata,proto3" json:"scalerMetadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ScaledObjectRef) Reset() {
|
||||
*x = ScaledObjectRef{}
|
||||
mi := &file_externalscaler_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ScaledObjectRef) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ScaledObjectRef) ProtoMessage() {}
|
||||
|
||||
func (x *ScaledObjectRef) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_externalscaler_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ScaledObjectRef.ProtoReflect.Descriptor instead.
|
||||
func (*ScaledObjectRef) Descriptor() ([]byte, []int) {
|
||||
return file_externalscaler_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *ScaledObjectRef) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ScaledObjectRef) GetNamespace() string {
|
||||
if x != nil {
|
||||
return x.Namespace
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ScaledObjectRef) GetScalerMetadata() map[string]string {
|
||||
if x != nil {
|
||||
return x.ScalerMetadata
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type IsActiveResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Result bool `protobuf:"varint,1,opt,name=result,proto3" json:"result,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *IsActiveResponse) Reset() {
|
||||
*x = IsActiveResponse{}
|
||||
mi := &file_externalscaler_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *IsActiveResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*IsActiveResponse) ProtoMessage() {}
|
||||
|
||||
func (x *IsActiveResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_externalscaler_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use IsActiveResponse.ProtoReflect.Descriptor instead.
|
||||
func (*IsActiveResponse) Descriptor() ([]byte, []int) {
|
||||
return file_externalscaler_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *IsActiveResponse) GetResult() bool {
|
||||
if x != nil {
|
||||
return x.Result
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type GetMetricSpecResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
MetricSpecs []*MetricSpec `protobuf:"bytes,1,rep,name=metricSpecs,proto3" json:"metricSpecs,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetMetricSpecResponse) Reset() {
|
||||
*x = GetMetricSpecResponse{}
|
||||
mi := &file_externalscaler_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetMetricSpecResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetMetricSpecResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetMetricSpecResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_externalscaler_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetMetricSpecResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetMetricSpecResponse) Descriptor() ([]byte, []int) {
|
||||
return file_externalscaler_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *GetMetricSpecResponse) GetMetricSpecs() []*MetricSpec {
|
||||
if x != nil {
|
||||
return x.MetricSpecs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type MetricSpec struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
MetricName string `protobuf:"bytes,1,opt,name=metricName,proto3" json:"metricName,omitempty"`
|
||||
TargetSize int64 `protobuf:"varint,2,opt,name=targetSize,proto3" json:"targetSize,omitempty"`
|
||||
TargetSizeFloat float64 `protobuf:"fixed64,3,opt,name=targetSizeFloat,proto3" json:"targetSizeFloat,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *MetricSpec) Reset() {
|
||||
*x = MetricSpec{}
|
||||
mi := &file_externalscaler_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *MetricSpec) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*MetricSpec) ProtoMessage() {}
|
||||
|
||||
func (x *MetricSpec) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_externalscaler_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use MetricSpec.ProtoReflect.Descriptor instead.
|
||||
func (*MetricSpec) Descriptor() ([]byte, []int) {
|
||||
return file_externalscaler_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *MetricSpec) GetMetricName() string {
|
||||
if x != nil {
|
||||
return x.MetricName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *MetricSpec) GetTargetSize() int64 {
|
||||
if x != nil {
|
||||
return x.TargetSize
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *MetricSpec) GetTargetSizeFloat() float64 {
|
||||
if x != nil {
|
||||
return x.TargetSizeFloat
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type GetMetricsRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ScaledObjectRef *ScaledObjectRef `protobuf:"bytes,1,opt,name=scaledObjectRef,proto3" json:"scaledObjectRef,omitempty"`
|
||||
MetricName string `protobuf:"bytes,2,opt,name=metricName,proto3" json:"metricName,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetMetricsRequest) Reset() {
|
||||
*x = GetMetricsRequest{}
|
||||
mi := &file_externalscaler_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetMetricsRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetMetricsRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetMetricsRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_externalscaler_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetMetricsRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetMetricsRequest) Descriptor() ([]byte, []int) {
|
||||
return file_externalscaler_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *GetMetricsRequest) GetScaledObjectRef() *ScaledObjectRef {
|
||||
if x != nil {
|
||||
return x.ScaledObjectRef
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetMetricsRequest) GetMetricName() string {
|
||||
if x != nil {
|
||||
return x.MetricName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type GetMetricsResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
MetricValues []*MetricValue `protobuf:"bytes,1,rep,name=metricValues,proto3" json:"metricValues,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetMetricsResponse) Reset() {
|
||||
*x = GetMetricsResponse{}
|
||||
mi := &file_externalscaler_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetMetricsResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetMetricsResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetMetricsResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_externalscaler_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetMetricsResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetMetricsResponse) Descriptor() ([]byte, []int) {
|
||||
return file_externalscaler_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *GetMetricsResponse) GetMetricValues() []*MetricValue {
|
||||
if x != nil {
|
||||
return x.MetricValues
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type MetricValue struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
MetricName string `protobuf:"bytes,1,opt,name=metricName,proto3" json:"metricName,omitempty"`
|
||||
MetricValue int64 `protobuf:"varint,2,opt,name=metricValue,proto3" json:"metricValue,omitempty"`
|
||||
MetricValueFloat float64 `protobuf:"fixed64,3,opt,name=metricValueFloat,proto3" json:"metricValueFloat,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *MetricValue) Reset() {
|
||||
*x = MetricValue{}
|
||||
mi := &file_externalscaler_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *MetricValue) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*MetricValue) ProtoMessage() {}
|
||||
|
||||
func (x *MetricValue) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_externalscaler_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use MetricValue.ProtoReflect.Descriptor instead.
|
||||
func (*MetricValue) Descriptor() ([]byte, []int) {
|
||||
return file_externalscaler_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *MetricValue) GetMetricName() string {
|
||||
if x != nil {
|
||||
return x.MetricName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *MetricValue) GetMetricValue() int64 {
|
||||
if x != nil {
|
||||
return x.MetricValue
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *MetricValue) GetMetricValueFloat() float64 {
|
||||
if x != nil {
|
||||
return x.MetricValueFloat
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var File_externalscaler_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_externalscaler_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x14externalscaler.proto\x12\x0eexternalscaler\"\xe3\x01\n" +
|
||||
"\x0fScaledObjectRef\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x1c\n" +
|
||||
"\tnamespace\x18\x02 \x01(\tR\tnamespace\x12[\n" +
|
||||
"\x0escalerMetadata\x18\x03 \x03(\v23.externalscaler.ScaledObjectRef.ScalerMetadataEntryR\x0escalerMetadata\x1aA\n" +
|
||||
"\x13ScalerMetadataEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"*\n" +
|
||||
"\x10IsActiveResponse\x12\x16\n" +
|
||||
"\x06result\x18\x01 \x01(\bR\x06result\"U\n" +
|
||||
"\x15GetMetricSpecResponse\x12<\n" +
|
||||
"\vmetricSpecs\x18\x01 \x03(\v2\x1a.externalscaler.MetricSpecR\vmetricSpecs\"v\n" +
|
||||
"\n" +
|
||||
"MetricSpec\x12\x1e\n" +
|
||||
"\n" +
|
||||
"metricName\x18\x01 \x01(\tR\n" +
|
||||
"metricName\x12\x1e\n" +
|
||||
"\n" +
|
||||
"targetSize\x18\x02 \x01(\x03R\n" +
|
||||
"targetSize\x12(\n" +
|
||||
"\x0ftargetSizeFloat\x18\x03 \x01(\x01R\x0ftargetSizeFloat\"~\n" +
|
||||
"\x11GetMetricsRequest\x12I\n" +
|
||||
"\x0fscaledObjectRef\x18\x01 \x01(\v2\x1f.externalscaler.ScaledObjectRefR\x0fscaledObjectRef\x12\x1e\n" +
|
||||
"\n" +
|
||||
"metricName\x18\x02 \x01(\tR\n" +
|
||||
"metricName\"U\n" +
|
||||
"\x12GetMetricsResponse\x12?\n" +
|
||||
"\fmetricValues\x18\x01 \x03(\v2\x1b.externalscaler.MetricValueR\fmetricValues\"{\n" +
|
||||
"\vMetricValue\x12\x1e\n" +
|
||||
"\n" +
|
||||
"metricName\x18\x01 \x01(\tR\n" +
|
||||
"metricName\x12 \n" +
|
||||
"\vmetricValue\x18\x02 \x01(\x03R\vmetricValue\x12*\n" +
|
||||
"\x10metricValueFloat\x18\x03 \x01(\x01R\x10metricValueFloat2\xec\x02\n" +
|
||||
"\x0eExternalScaler\x12O\n" +
|
||||
"\bIsActive\x12\x1f.externalscaler.ScaledObjectRef\x1a .externalscaler.IsActiveResponse\"\x00\x12W\n" +
|
||||
"\x0eStreamIsActive\x12\x1f.externalscaler.ScaledObjectRef\x1a .externalscaler.IsActiveResponse\"\x000\x01\x12Y\n" +
|
||||
"\rGetMetricSpec\x12\x1f.externalscaler.ScaledObjectRef\x1a%.externalscaler.GetMetricSpecResponse\"\x00\x12U\n" +
|
||||
"\n" +
|
||||
"GetMetrics\x12!.externalscaler.GetMetricsRequest\x1a\".externalscaler.GetMetricsResponse\"\x00B\x12Z\x10.;externalscalerb\x06proto3"
|
||||
|
||||
var (
|
||||
file_externalscaler_proto_rawDescOnce sync.Once
|
||||
file_externalscaler_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_externalscaler_proto_rawDescGZIP() []byte {
|
||||
file_externalscaler_proto_rawDescOnce.Do(func() {
|
||||
file_externalscaler_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_externalscaler_proto_rawDesc), len(file_externalscaler_proto_rawDesc)))
|
||||
})
|
||||
return file_externalscaler_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_externalscaler_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
|
||||
var file_externalscaler_proto_goTypes = []any{
|
||||
(*ScaledObjectRef)(nil), // 0: externalscaler.ScaledObjectRef
|
||||
(*IsActiveResponse)(nil), // 1: externalscaler.IsActiveResponse
|
||||
(*GetMetricSpecResponse)(nil), // 2: externalscaler.GetMetricSpecResponse
|
||||
(*MetricSpec)(nil), // 3: externalscaler.MetricSpec
|
||||
(*GetMetricsRequest)(nil), // 4: externalscaler.GetMetricsRequest
|
||||
(*GetMetricsResponse)(nil), // 5: externalscaler.GetMetricsResponse
|
||||
(*MetricValue)(nil), // 6: externalscaler.MetricValue
|
||||
nil, // 7: externalscaler.ScaledObjectRef.ScalerMetadataEntry
|
||||
}
|
||||
var file_externalscaler_proto_depIdxs = []int32{
|
||||
7, // 0: externalscaler.ScaledObjectRef.scalerMetadata:type_name -> externalscaler.ScaledObjectRef.ScalerMetadataEntry
|
||||
3, // 1: externalscaler.GetMetricSpecResponse.metricSpecs:type_name -> externalscaler.MetricSpec
|
||||
0, // 2: externalscaler.GetMetricsRequest.scaledObjectRef:type_name -> externalscaler.ScaledObjectRef
|
||||
6, // 3: externalscaler.GetMetricsResponse.metricValues:type_name -> externalscaler.MetricValue
|
||||
0, // 4: externalscaler.ExternalScaler.IsActive:input_type -> externalscaler.ScaledObjectRef
|
||||
0, // 5: externalscaler.ExternalScaler.StreamIsActive:input_type -> externalscaler.ScaledObjectRef
|
||||
0, // 6: externalscaler.ExternalScaler.GetMetricSpec:input_type -> externalscaler.ScaledObjectRef
|
||||
4, // 7: externalscaler.ExternalScaler.GetMetrics:input_type -> externalscaler.GetMetricsRequest
|
||||
1, // 8: externalscaler.ExternalScaler.IsActive:output_type -> externalscaler.IsActiveResponse
|
||||
1, // 9: externalscaler.ExternalScaler.StreamIsActive:output_type -> externalscaler.IsActiveResponse
|
||||
2, // 10: externalscaler.ExternalScaler.GetMetricSpec:output_type -> externalscaler.GetMetricSpecResponse
|
||||
5, // 11: externalscaler.ExternalScaler.GetMetrics:output_type -> externalscaler.GetMetricsResponse
|
||||
8, // [8:12] is the sub-list for method output_type
|
||||
4, // [4:8] is the sub-list for method input_type
|
||||
4, // [4:4] is the sub-list for extension type_name
|
||||
4, // [4:4] is the sub-list for extension extendee
|
||||
0, // [0:4] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_externalscaler_proto_init() }
|
||||
func file_externalscaler_proto_init() {
|
||||
if File_externalscaler_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_externalscaler_proto_rawDesc), len(file_externalscaler_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 8,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_externalscaler_proto_goTypes,
|
||||
DependencyIndexes: file_externalscaler_proto_depIdxs,
|
||||
MessageInfos: file_externalscaler_proto_msgTypes,
|
||||
}.Build()
|
||||
File_externalscaler_proto = out.File
|
||||
file_externalscaler_proto_goTypes = nil
|
||||
file_externalscaler_proto_depIdxs = nil
|
||||
}
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v6.33.4
|
||||
// source: externalscaler.proto
|
||||
|
||||
package externalscaler
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
ExternalScaler_IsActive_FullMethodName = "/externalscaler.ExternalScaler/IsActive"
|
||||
ExternalScaler_StreamIsActive_FullMethodName = "/externalscaler.ExternalScaler/StreamIsActive"
|
||||
ExternalScaler_GetMetricSpec_FullMethodName = "/externalscaler.ExternalScaler/GetMetricSpec"
|
||||
ExternalScaler_GetMetrics_FullMethodName = "/externalscaler.ExternalScaler/GetMetrics"
|
||||
)
|
||||
|
||||
// ExternalScalerClient is the client API for ExternalScaler service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type ExternalScalerClient interface {
|
||||
IsActive(ctx context.Context, in *ScaledObjectRef, opts ...grpc.CallOption) (*IsActiveResponse, error)
|
||||
StreamIsActive(ctx context.Context, in *ScaledObjectRef, opts ...grpc.CallOption) (grpc.ServerStreamingClient[IsActiveResponse], error)
|
||||
GetMetricSpec(ctx context.Context, in *ScaledObjectRef, opts ...grpc.CallOption) (*GetMetricSpecResponse, error)
|
||||
GetMetrics(ctx context.Context, in *GetMetricsRequest, opts ...grpc.CallOption) (*GetMetricsResponse, error)
|
||||
}
|
||||
|
||||
type externalScalerClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewExternalScalerClient(cc grpc.ClientConnInterface) ExternalScalerClient {
|
||||
return &externalScalerClient{cc}
|
||||
}
|
||||
|
||||
func (c *externalScalerClient) IsActive(ctx context.Context, in *ScaledObjectRef, opts ...grpc.CallOption) (*IsActiveResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(IsActiveResponse)
|
||||
err := c.cc.Invoke(ctx, ExternalScaler_IsActive_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *externalScalerClient) StreamIsActive(ctx context.Context, in *ScaledObjectRef, opts ...grpc.CallOption) (grpc.ServerStreamingClient[IsActiveResponse], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &ExternalScaler_ServiceDesc.Streams[0], ExternalScaler_StreamIsActive_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[ScaledObjectRef, IsActiveResponse]{ClientStream: stream}
|
||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := x.ClientStream.CloseSend(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type ExternalScaler_StreamIsActiveClient = grpc.ServerStreamingClient[IsActiveResponse]
|
||||
|
||||
func (c *externalScalerClient) GetMetricSpec(ctx context.Context, in *ScaledObjectRef, opts ...grpc.CallOption) (*GetMetricSpecResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetMetricSpecResponse)
|
||||
err := c.cc.Invoke(ctx, ExternalScaler_GetMetricSpec_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *externalScalerClient) GetMetrics(ctx context.Context, in *GetMetricsRequest, opts ...grpc.CallOption) (*GetMetricsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetMetricsResponse)
|
||||
err := c.cc.Invoke(ctx, ExternalScaler_GetMetrics_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ExternalScalerServer is the server API for ExternalScaler service.
|
||||
// All implementations must embed UnimplementedExternalScalerServer
|
||||
// for forward compatibility.
|
||||
type ExternalScalerServer interface {
|
||||
IsActive(context.Context, *ScaledObjectRef) (*IsActiveResponse, error)
|
||||
StreamIsActive(*ScaledObjectRef, grpc.ServerStreamingServer[IsActiveResponse]) error
|
||||
GetMetricSpec(context.Context, *ScaledObjectRef) (*GetMetricSpecResponse, error)
|
||||
GetMetrics(context.Context, *GetMetricsRequest) (*GetMetricsResponse, error)
|
||||
mustEmbedUnimplementedExternalScalerServer()
|
||||
}
|
||||
|
||||
// UnimplementedExternalScalerServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedExternalScalerServer struct{}
|
||||
|
||||
func (UnimplementedExternalScalerServer) IsActive(context.Context, *ScaledObjectRef) (*IsActiveResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method IsActive not implemented")
|
||||
}
|
||||
func (UnimplementedExternalScalerServer) StreamIsActive(*ScaledObjectRef, grpc.ServerStreamingServer[IsActiveResponse]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method StreamIsActive not implemented")
|
||||
}
|
||||
func (UnimplementedExternalScalerServer) GetMetricSpec(context.Context, *ScaledObjectRef) (*GetMetricSpecResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetMetricSpec not implemented")
|
||||
}
|
||||
func (UnimplementedExternalScalerServer) GetMetrics(context.Context, *GetMetricsRequest) (*GetMetricsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetMetrics not implemented")
|
||||
}
|
||||
func (UnimplementedExternalScalerServer) mustEmbedUnimplementedExternalScalerServer() {}
|
||||
func (UnimplementedExternalScalerServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeExternalScalerServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to ExternalScalerServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeExternalScalerServer interface {
|
||||
mustEmbedUnimplementedExternalScalerServer()
|
||||
}
|
||||
|
||||
func RegisterExternalScalerServer(s grpc.ServiceRegistrar, srv ExternalScalerServer) {
|
||||
// If the following call pancis, it indicates UnimplementedExternalScalerServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&ExternalScaler_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _ExternalScaler_IsActive_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ScaledObjectRef)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ExternalScalerServer).IsActive(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: ExternalScaler_IsActive_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ExternalScalerServer).IsActive(ctx, req.(*ScaledObjectRef))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ExternalScaler_StreamIsActive_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(ScaledObjectRef)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(ExternalScalerServer).StreamIsActive(m, &grpc.GenericServerStream[ScaledObjectRef, IsActiveResponse]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type ExternalScaler_StreamIsActiveServer = grpc.ServerStreamingServer[IsActiveResponse]
|
||||
|
||||
func _ExternalScaler_GetMetricSpec_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ScaledObjectRef)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ExternalScalerServer).GetMetricSpec(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: ExternalScaler_GetMetricSpec_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ExternalScalerServer).GetMetricSpec(ctx, req.(*ScaledObjectRef))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ExternalScaler_GetMetrics_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetMetricsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ExternalScalerServer).GetMetrics(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: ExternalScaler_GetMetrics_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ExternalScalerServer).GetMetrics(ctx, req.(*GetMetricsRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// ExternalScaler_ServiceDesc is the grpc.ServiceDesc for ExternalScaler service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var ExternalScaler_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "externalscaler.ExternalScaler",
|
||||
HandlerType: (*ExternalScalerServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "IsActive",
|
||||
Handler: _ExternalScaler_IsActive_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetMetricSpec",
|
||||
Handler: _ExternalScaler_GetMetricSpec_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetMetrics",
|
||||
Handler: _ExternalScaler_GetMetrics_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
StreamName: "StreamIsActive",
|
||||
Handler: _ExternalScaler_StreamIsActive_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "externalscaler.proto",
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
module gitea-scaler
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/redis/go-redis/v9 v9.12.1
|
||||
google.golang.org/grpc v1.76.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
|
||||
)
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/redis/go-redis/v9 v9.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg=
|
||||
github.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
|
|
@ -0,0 +1,374 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"google.golang.org/grpc"
|
||||
pb "gitea-scaler/externalscaler"
|
||||
)
|
||||
|
||||
const (
|
||||
metricName = "gitea_waiting_jobs"
|
||||
jobKeyPrefix = "gitea-scaler:job:"
|
||||
labelCountPrefix = "gitea-scaler:label:"
|
||||
)
|
||||
|
||||
type workflowJobPayload struct {
|
||||
Action string `json:"action"`
|
||||
WorkflowJob struct {
|
||||
ID int64 `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Labels []string `json:"labels"`
|
||||
} `json:"workflow_job"`
|
||||
}
|
||||
|
||||
type scalerServer struct {
|
||||
pb.UnimplementedExternalScalerServer
|
||||
redis *redis.Client
|
||||
}
|
||||
|
||||
func main() {
|
||||
redisClient := redis.NewClient(&redis.Options{
|
||||
Addr: getEnv("REDIS_ADDR", "redis.jam-cloud-infra.svc.cluster.local:6379"),
|
||||
DB: mustAtoi(getEnv("REDIS_DB", "1")),
|
||||
})
|
||||
|
||||
if err := redisClient.Ping(context.Background()).Err(); err != nil {
|
||||
log.Fatalf("failed to connect to redis: %v", err)
|
||||
}
|
||||
|
||||
server := &scalerServer{redis: redisClient}
|
||||
|
||||
go serveGRPC(server)
|
||||
go serveHTTP(server)
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
func serveGRPC(server *scalerServer) {
|
||||
grpcServer := grpc.NewServer()
|
||||
pb.RegisterExternalScalerServer(grpcServer, server)
|
||||
|
||||
lis, err := net.Listen("tcp", getEnv("GRPC_ADDR", ":50051"))
|
||||
if err != nil {
|
||||
log.Fatalf("failed to listen for grpc: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("gRPC scaler listening on %s", lis.Addr())
|
||||
if err := grpcServer.Serve(lis); err != nil {
|
||||
log.Fatalf("failed to serve grpc: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func serveHTTP(server *scalerServer) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
})
|
||||
mux.HandleFunc("/webhooks/workflow-job", server.handleWorkflowJob)
|
||||
|
||||
addr := getEnv("HTTP_ADDR", ":8080")
|
||||
log.Printf("webhook server listening on %s", addr)
|
||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||
log.Fatalf("failed to serve http: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *scalerServer) handleWorkflowJob(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
event := r.Header.Get("X-Gitea-Event")
|
||||
if event == "" {
|
||||
event = r.Header.Get("X-GitHub-Event")
|
||||
}
|
||||
if event != "workflow_job" {
|
||||
http.Error(w, "unsupported event", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to read request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := verifySignature(body, r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var payload workflowJobPayload
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
http.Error(w, "invalid json payload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if payload.WorkflowJob.ID == 0 {
|
||||
http.Error(w, "missing workflow_job.id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
switch normalizeAction(payload) {
|
||||
case "queued":
|
||||
if err := s.markQueued(r.Context(), payload.WorkflowJob.ID, payload.WorkflowJob.Labels); err != nil {
|
||||
http.Error(w, "failed to record queued job", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
case "remove":
|
||||
if err := s.markComplete(r.Context(), payload.WorkflowJob.ID); err != nil {
|
||||
http.Error(w, "failed to remove completed job", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
default:
|
||||
log.Printf("ignoring workflow_job action=%q status=%q id=%d", payload.Action, payload.WorkflowJob.Status, payload.WorkflowJob.ID)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
|
||||
func (s *scalerServer) IsActive(ctx context.Context, ref *pb.ScaledObjectRef) (*pb.IsActiveResponse, error) {
|
||||
count, err := s.countForLabels(ctx, labelsFromRef(ref))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.IsActiveResponse{Result: count > 0}, nil
|
||||
}
|
||||
|
||||
func (s *scalerServer) StreamIsActive(ref *pb.ScaledObjectRef, stream pb.ExternalScaler_StreamIsActiveServer) error {
|
||||
ticker := time.NewTicker(15 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
count, err := s.countForLabels(stream.Context(), labelsFromRef(ref))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := stream.Send(&pb.IsActiveResponse{Result: count > 0}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-stream.Context().Done():
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *scalerServer) GetMetricSpec(context.Context, *pb.ScaledObjectRef) (*pb.GetMetricSpecResponse, error) {
|
||||
return &pb.GetMetricSpecResponse{
|
||||
MetricSpecs: []*pb.MetricSpec{
|
||||
{
|
||||
MetricName: metricName,
|
||||
TargetSize: 1,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *scalerServer) GetMetrics(ctx context.Context, req *pb.GetMetricsRequest) (*pb.GetMetricsResponse, error) {
|
||||
count, err := s.countForLabels(ctx, labelsFromRef(req.GetScaledObjectRef()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pb.GetMetricsResponse{
|
||||
MetricValues: []*pb.MetricValue{
|
||||
{
|
||||
MetricName: metricName,
|
||||
MetricValue: int64(count),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *scalerServer) markQueued(ctx context.Context, jobID int64, labels []string) error {
|
||||
jobKey := fmt.Sprintf("%s%d", jobKeyPrefix, jobID)
|
||||
|
||||
existingLabels, err := s.redis.SMembers(ctx, jobKey).Result()
|
||||
if err != nil && !errors.Is(err, redis.Nil) {
|
||||
return err
|
||||
}
|
||||
|
||||
pipe := s.redis.TxPipeline()
|
||||
|
||||
if len(existingLabels) > 0 {
|
||||
for _, label := range existingLabels {
|
||||
pipe.Decr(ctx, labelKey(label))
|
||||
}
|
||||
pipe.Del(ctx, jobKey)
|
||||
}
|
||||
|
||||
if len(labels) > 0 {
|
||||
members := make([]interface{}, 0, len(labels))
|
||||
for _, label := range labels {
|
||||
normalized := strings.TrimSpace(label)
|
||||
if normalized == "" {
|
||||
continue
|
||||
}
|
||||
members = append(members, normalized)
|
||||
pipe.Incr(ctx, labelKey(normalized))
|
||||
}
|
||||
if len(members) > 0 {
|
||||
pipe.SAdd(ctx, jobKey, members...)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *scalerServer) markComplete(ctx context.Context, jobID int64) error {
|
||||
jobKey := fmt.Sprintf("%s%d", jobKeyPrefix, jobID)
|
||||
labels, err := s.redis.SMembers(ctx, jobKey).Result()
|
||||
if err != nil && !errors.Is(err, redis.Nil) {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(labels) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pipe := s.redis.TxPipeline()
|
||||
for _, label := range labels {
|
||||
pipe.Decr(ctx, labelKey(label))
|
||||
}
|
||||
pipe.Del(ctx, jobKey)
|
||||
_, err = pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *scalerServer) countForLabels(ctx context.Context, labels []string) (int, error) {
|
||||
if len(labels) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(labels))
|
||||
for _, label := range labels {
|
||||
keys = append(keys, labelKey(label))
|
||||
}
|
||||
|
||||
values, err := s.redis.MGet(ctx, keys...).Result()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
total := 0
|
||||
for _, value := range values {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
case string:
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
total += max(n, 0)
|
||||
case int64:
|
||||
total += max(int(v), 0)
|
||||
default:
|
||||
return 0, fmt.Errorf("unexpected redis value type %T", value)
|
||||
}
|
||||
}
|
||||
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func verifySignature(body []byte, r *http.Request) error {
|
||||
secret := os.Getenv("WEBHOOK_SECRET")
|
||||
if secret == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
signature := r.Header.Get("X-Gitea-Signature")
|
||||
if signature == "" {
|
||||
signature = r.Header.Get("X-Hub-Signature-256")
|
||||
signature = strings.TrimPrefix(signature, "sha256=")
|
||||
}
|
||||
if signature == "" {
|
||||
return errors.New("missing webhook signature header")
|
||||
}
|
||||
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write(body)
|
||||
expected := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
if !hmac.Equal([]byte(expected), []byte(strings.ToLower(signature))) {
|
||||
return errors.New("invalid webhook signature")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func labelsFromRef(ref *pb.ScaledObjectRef) []string {
|
||||
if ref == nil {
|
||||
return nil
|
||||
}
|
||||
labels := strings.Split(ref.ScalerMetadata["labels"], ",")
|
||||
result := make([]string, 0, len(labels))
|
||||
for _, label := range labels {
|
||||
normalized := strings.TrimSpace(label)
|
||||
if normalized != "" {
|
||||
result = append(result, normalized)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func normalizeAction(payload workflowJobPayload) string {
|
||||
action := strings.ToLower(strings.TrimSpace(payload.Action))
|
||||
status := strings.ToLower(strings.TrimSpace(payload.WorkflowJob.Status))
|
||||
|
||||
switch action {
|
||||
case "queued":
|
||||
return "queued"
|
||||
case "in_progress", "completed":
|
||||
return "remove"
|
||||
}
|
||||
|
||||
switch status {
|
||||
case "queued":
|
||||
return "queued"
|
||||
case "in_progress", "completed":
|
||||
return "remove"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func labelKey(label string) string {
|
||||
return fmt.Sprintf("%s%s", labelCountPrefix, label)
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func mustAtoi(value string) int {
|
||||
n, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid integer %q: %v", value, err)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue