From 7c5357e68d216992baccf48536d7a98fa0bfcfd5 Mon Sep 17 00:00:00 2001 From: kahirokunn Date: Thu, 12 Dec 2024 09:33:31 +0900 Subject: [PATCH 1/4] fix: correct typo in AutoscalerReference type name - Fix spelling of AutoscalerReference (was AutoscalerRefernce) in type definition and struct field Signed-off-by: kahirokunn --- pkg/apis/flagger/v1beta1/canary.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/apis/flagger/v1beta1/canary.go b/pkg/apis/flagger/v1beta1/canary.go index f5797e876..8c5ac5393 100644 --- a/pkg/apis/flagger/v1beta1/canary.go +++ b/pkg/apis/flagger/v1beta1/canary.go @@ -74,7 +74,7 @@ type CanarySpec struct { // AutoscalerRef references an autoscaling resource // +optional - AutoscalerRef *AutoscalerRefernce `json:"autoscalerRef,omitempty"` + AutoscalerRef *AutoscalerReference `json:"autoscalerRef,omitempty"` // Reference to NGINX ingress resource // +optional @@ -458,7 +458,7 @@ type LocalObjectReference struct { Name string `json:"name"` } -type AutoscalerRefernce struct { +type AutoscalerReference struct { // API version of the scaler // +required APIVersion string `json:"apiVersion,omitempty"` From 2c51b03814c842ff35fa37b979733dce3f6219e2 Mon Sep 17 00:00:00 2001 From: kahirokunn Date: Fri, 25 Oct 2024 14:18:00 +0900 Subject: [PATCH 2/4] add support for KEDA HTTPScaledObjects via HTTPScaledObjectReconciler Signed-off-by: kahirokunn --- artifacts/flagger/account.yaml | 13 + artifacts/flagger/crd.yaml | 33 + charts/flagger/crds/crd.yaml | 33 + charts/flagger/templates/rbac.yaml | 13 + docs/diagrams/flagger-keda-http-add-on.png | Bin 0 -> 218301 bytes docs/gitbook/SUMMARY.md | 1 + .../tutorials/keda-httpscaledobject.md | 760 ++++++++++++++++++ kustomize/base/flagger/crd.yaml | 33 + kustomize/base/flagger/rbac.yaml | 13 + pkg/apis/flagger/v1beta1/canary.go | 45 ++ .../flagger/v1beta1/zz_generated.deepcopy.go | 42 + pkg/apis/http/register.go | 5 + pkg/apis/http/v1alpha1/condition_types.go | 88 ++ pkg/apis/http/v1alpha1/doc.go | 5 + .../http/v1alpha1/httpscaledobject_types.go | 183 +++++ .../http/v1alpha1/httpscalingset_defaults.go | 149 ++++ .../http/v1alpha1/httpscalingset_types.go | 233 ++++++ pkg/apis/http/v1alpha1/register.go | 56 ++ pkg/apis/http/v1alpha1/version.go | 30 + .../http/v1alpha1/zz_generated.deepcopy.go | 687 ++++++++++++++++ pkg/canary/deployment_controller.go | 16 +- pkg/canary/factory.go | 9 + pkg/canary/http_scaled_object_reconciler.go | 130 +++ pkg/client/clientset/versioned/clientset.go | 13 + .../versioned/fake/clientset_generated.go | 7 + .../clientset/versioned/fake/register.go | 2 + .../clientset/versioned/scheme/register.go | 2 + .../http/v1alpha1/clusterhttpscalingset.go | 69 ++ .../versioned/typed/http/v1alpha1/doc.go | 20 + .../versioned/typed/http/v1alpha1/fake/doc.go | 20 + .../fake/fake_clusterhttpscalingset.go | 147 ++++ .../http/v1alpha1/fake/fake_http_client.go | 48 ++ .../v1alpha1/fake/fake_httpscaledobject.go | 147 ++++ .../http/v1alpha1/fake/fake_httpscalingset.go | 147 ++++ .../http/v1alpha1/generated_expansion.go | 25 + .../typed/http/v1alpha1/http_client.go | 117 +++ .../typed/http/v1alpha1/httpscaledobject.go | 69 ++ .../typed/http/v1alpha1/httpscalingset.go | 69 ++ .../informers/externalversions/factory.go | 6 + .../informers/externalversions/generic.go | 13 +- .../externalversions/http/interface.go | 46 ++ .../http/v1alpha1/clusterhttpscalingset.go | 90 +++ .../http/v1alpha1/httpscaledobject.go | 90 +++ .../http/v1alpha1/httpscalingset.go | 90 +++ .../http/v1alpha1/interface.go | 59 ++ .../http/v1alpha1/clusterhttpscalingset.go | 70 ++ .../http/v1alpha1/expansion_generated.go | 43 + .../listers/http/v1alpha1/httpscaledobject.go | 70 ++ .../listers/http/v1alpha1/httpscalingset.go | 70 ++ pkg/router/factory.go | 20 +- pkg/router/gateway_api.go | 104 ++- pkg/router/kubernetes_default.go | 18 +- 52 files changed, 4223 insertions(+), 45 deletions(-) create mode 100644 docs/diagrams/flagger-keda-http-add-on.png create mode 100644 docs/gitbook/tutorials/keda-httpscaledobject.md create mode 100644 pkg/apis/http/register.go create mode 100644 pkg/apis/http/v1alpha1/condition_types.go create mode 100644 pkg/apis/http/v1alpha1/doc.go create mode 100644 pkg/apis/http/v1alpha1/httpscaledobject_types.go create mode 100644 pkg/apis/http/v1alpha1/httpscalingset_defaults.go create mode 100644 pkg/apis/http/v1alpha1/httpscalingset_types.go create mode 100644 pkg/apis/http/v1alpha1/register.go create mode 100644 pkg/apis/http/v1alpha1/version.go create mode 100644 pkg/apis/http/v1alpha1/zz_generated.deepcopy.go create mode 100644 pkg/canary/http_scaled_object_reconciler.go create mode 100644 pkg/client/clientset/versioned/typed/http/v1alpha1/clusterhttpscalingset.go create mode 100644 pkg/client/clientset/versioned/typed/http/v1alpha1/doc.go create mode 100644 pkg/client/clientset/versioned/typed/http/v1alpha1/fake/doc.go create mode 100644 pkg/client/clientset/versioned/typed/http/v1alpha1/fake/fake_clusterhttpscalingset.go create mode 100644 pkg/client/clientset/versioned/typed/http/v1alpha1/fake/fake_http_client.go create mode 100644 pkg/client/clientset/versioned/typed/http/v1alpha1/fake/fake_httpscaledobject.go create mode 100644 pkg/client/clientset/versioned/typed/http/v1alpha1/fake/fake_httpscalingset.go create mode 100644 pkg/client/clientset/versioned/typed/http/v1alpha1/generated_expansion.go create mode 100644 pkg/client/clientset/versioned/typed/http/v1alpha1/http_client.go create mode 100644 pkg/client/clientset/versioned/typed/http/v1alpha1/httpscaledobject.go create mode 100644 pkg/client/clientset/versioned/typed/http/v1alpha1/httpscalingset.go create mode 100644 pkg/client/informers/externalversions/http/interface.go create mode 100644 pkg/client/informers/externalversions/http/v1alpha1/clusterhttpscalingset.go create mode 100644 pkg/client/informers/externalversions/http/v1alpha1/httpscaledobject.go create mode 100644 pkg/client/informers/externalversions/http/v1alpha1/httpscalingset.go create mode 100644 pkg/client/informers/externalversions/http/v1alpha1/interface.go create mode 100644 pkg/client/listers/http/v1alpha1/clusterhttpscalingset.go create mode 100644 pkg/client/listers/http/v1alpha1/expansion_generated.go create mode 100644 pkg/client/listers/http/v1alpha1/httpscaledobject.go create mode 100644 pkg/client/listers/http/v1alpha1/httpscalingset.go diff --git a/artifacts/flagger/account.yaml b/artifacts/flagger/account.yaml index ae07a7f89..79f0aae14 100644 --- a/artifacts/flagger/account.yaml +++ b/artifacts/flagger/account.yaml @@ -239,6 +239,19 @@ rules: - update - patch - delete + - apiGroups: + - http.keda.sh + resources: + - httpscaledobjects + - httpscaledobjects/finalizers + verbs: + - get + - list + - watch + - create + - update + - patch + - delete - apiGroups: - apisix.apache.org resources: diff --git a/artifacts/flagger/crd.yaml b/artifacts/flagger/crd.yaml index 39a466dd2..f749244c3 100644 --- a/artifacts/flagger/crd.yaml +++ b/artifacts/flagger/crd.yaml @@ -119,6 +119,7 @@ spec: enum: - HorizontalPodAutoscaler - ScaledObject + - HTTPScaledObject name: type: string primaryScalerQueries: @@ -134,6 +135,38 @@ spec: maxReplicas: type: integer minimum: 1 + canaryInterceptorProxyService: + type: object + description: Specify this service if you want to change the Canary interceptor proxy service from its default value. + properties: + name: + default: keda-http-add-on-interceptor-proxy + maxLength: 253 + minLength: 1 + type: string + namespace: + default: keda + maxLength: 63 + minLength: 1 + type: string + primaryScalingSet: + type: object + description: |- + PrimaryScalingSet to be used for primary HTTPScaledObject, if empty, default interceptor and scaler will be used. + properties: + kind: + description: Kind of the resource being referred to. Defaults to HTTPScalingSet. + enum: + - HTTPScalingSet + - ClusterHTTPScalingSet + type: string + name: + description: Name of the scaling set + type: string + namespace: + maxLength: 63 + minLength: 1 + type: string ingressRef: description: Ingress selector type: object diff --git a/charts/flagger/crds/crd.yaml b/charts/flagger/crds/crd.yaml index 39a466dd2..f749244c3 100644 --- a/charts/flagger/crds/crd.yaml +++ b/charts/flagger/crds/crd.yaml @@ -119,6 +119,7 @@ spec: enum: - HorizontalPodAutoscaler - ScaledObject + - HTTPScaledObject name: type: string primaryScalerQueries: @@ -134,6 +135,38 @@ spec: maxReplicas: type: integer minimum: 1 + canaryInterceptorProxyService: + type: object + description: Specify this service if you want to change the Canary interceptor proxy service from its default value. + properties: + name: + default: keda-http-add-on-interceptor-proxy + maxLength: 253 + minLength: 1 + type: string + namespace: + default: keda + maxLength: 63 + minLength: 1 + type: string + primaryScalingSet: + type: object + description: |- + PrimaryScalingSet to be used for primary HTTPScaledObject, if empty, default interceptor and scaler will be used. + properties: + kind: + description: Kind of the resource being referred to. Defaults to HTTPScalingSet. + enum: + - HTTPScalingSet + - ClusterHTTPScalingSet + type: string + name: + description: Name of the scaling set + type: string + namespace: + maxLength: 63 + minLength: 1 + type: string ingressRef: description: Ingress selector type: object diff --git a/charts/flagger/templates/rbac.yaml b/charts/flagger/templates/rbac.yaml index ae9c70155..6085fd8b3 100644 --- a/charts/flagger/templates/rbac.yaml +++ b/charts/flagger/templates/rbac.yaml @@ -247,6 +247,19 @@ rules: - update - patch - delete + - apiGroups: + - http.keda.sh + resources: + - httpscaledobjects + - httpscaledobjects/finalizers + verbs: + - get + - list + - watch + - create + - update + - patch + - delete - apiGroups: - apisix.apache.org resources: diff --git a/docs/diagrams/flagger-keda-http-add-on.png b/docs/diagrams/flagger-keda-http-add-on.png new file mode 100644 index 0000000000000000000000000000000000000000..2c4adeb9b7c8487d66527b835977c8e4d18d1418 GIT binary patch literal 218301 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�Ma;wzg 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≶qAi9Z83&r3j%WG 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 P#oKy+v?*@ELXGt zcGpm4re+hB5X#Q|sMO{(AfYBRaLoB|4ERLWBc&e#l_9uSX(Zr)>g)ypec8qVQ;E<( zo8i|B{I{kb2ot>q(l?stwK1pmyoY!^%@V4bU-sGPylWBLw|ZvLXCpJXc;f}m`@N5e zNnK)JOO&NXWX({Hc!yZT;nv{K$g((Gd{eMFy*FH6oTSoeX1Qd8s!_vw;7~T+9Nlr# z{S@^nOnU_Ri*{|1?nW78-=ucL))x{o|D`OR6hNJuOYqnYL1MH*bL|A;wtKM$VHt5) zV+$$MbQ{=(Dag6ba+1IK&3#S-(*$=02(IbJ>b`E4aq=%iH%wI6sm35S0Ek8gVXu>J z>`GIY98S{%*t^F4s7D-zJ{O!dr>XPj8&V7Ui<|>ZJ|<2!Jv9&Y3keKvT{{aqZEN|! zbVQO^gR&t&AUM@`K?U<$gFbPWLqfuLmdUf{Ket6?v@#v7?#MT@;N)E6xZ<;_kKI_| zX!qeLjHmLhz`ZzHd^pT~_|OICip7f{ELFl(M(OJC(_{?^%nfQK$!wWm4*y1huS7N# z;alCUek^SPu9|<^Z190MeX(R|5n@;U1xDD|vuxLS(4zQ+J0`JrIXAsI=HuplI23%^ z#p39nY`8B67a_I3)~t(5x~3FdELQUg)UZP{pMP~@lkq-zl^oomg_$#r=Xw)AE&J{r zvOs`gwLUY~;}Hk%kwHo(jA;Nx8UijK`3q#a`|i+SX~9PLueeTgV(o0s;`k_PE%tJP zxlJZ1`5gBCO@We(>JxtreKP`u*P<%WRAl!H1_9eK-QBU3?QG{;vm5-C^_ zD&C51VL89JV2U=s$3K6aX`S=in(b+QeUkGdEX*s-bASupIwhvP&|OZnW;8j-u@=|b zs@+vH>V^vL-xwUYt7L-2KMI6Hj6b_i&u46G+J1$Vw0;@y+&aOeY>$Wee875Jm}5*o z%RABrc6U_cq!&L?2I#URwmks|%&vA6O*L*5?p0oKd7!+8ZwjoOZ@J^*7DygtH{xHv zbGfx|NE=s@xMS>cn-D1v1@bj$eT1BPOpa?jm<`sm?z@_?SLr18YK**-O zw9SGCW>A~9X6$a15pfw{ZI|@g(Js3IEH}C!(x~VI?Ke5PSdT49d-R?``e;UT(0?^~ z5sXA?7i};O#?P;l+bwTK;{%%BGby8+ICCvN1Xqixca)9G7+w<}XI#&dAmTi)Tt ztfN&hQafCawl=1~+j;U%?9TCiTDKfpnodEKVs_(r_XrYQd+$0) zi`ekj*FBnd$4~C|G^|^yO$h9Hve7SKq5j8_=>)c7|8blbw%U6W+Fyfy2S)fGN2b$% z1SUWJ`Ystf&_9mfe_rnA*!@E$=za>yPp$Z=6+gA&A0zP7R{XTHd$XNC#N(%}_-QMC z+KPXyls`SFpSI$st@vpx{<$pt|DYA4yQQ87f28k1wmy)SIIjM9$hzeJPAkaQ@7=cg zahh}Re`OH*4(u(F_;J?z!~Zq)#>Z(j7XNX46#g%}VWs Note: The above installation sets the mesh provider to be `gatewayapi:v1`. If your Gateway API implementation uses the `v1beta1` CRDs, then +set the `--meshProvider` value to `gatewayapi:v1beta1`. + +Create a namespace for the `Gateway`: + +```bash +kubectl create ns istio-ingress +``` + +Create a `Gateway` that configures load balancing, traffic ACL, etc: + +```yaml +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: Gateway +metadata: + name: gateway + namespace: istio-ingress +spec: + gatewayClassName: istio + listeners: + - name: default + hostname: "*.example.com" + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: All +``` + +Create a interceptor for the primary workloads: + +```yaml +kind: ClusterHTTPScalingSet +apiVersion: http.keda.sh/v1alpha1 +metadata: + name: primary +spec: + interceptor: + config: + adminPort: 9090 + connectTimeout: 500ms + expectContinueTimeout: 1s + forceHTTP2: false + handshakeTimeout: 10s + headerTimeout: 500ms + idleConnTimeout: 90s + keepAlive: 1s + maxIdleConnections: 100 + pollingInterval: 1000 + proxyPort: 8080 + waitTimeout: 20s + replicas: 1 + resources: {} + serviceAccountName: keda-add-ons-http-interceptor + scaler: + serviceAccountName: keda-add-ons-http-external-scaler +``` + +## Bootstrap + +Flagger takes a Kubernetes deployment and a KEDA HTTPScaledObject targeting the deployment. It then creates a series of objects +(Kubernetes deployments, ClusterIP services targeting the Deployments, another KEDA HTTPScaledObject targeting the created Deployment and ClusterIP service, HTTPRoute targeting the KEDA interceptor). +These objects expose the application inside the mesh and drive the Canary analysis and Blue/Green promotion. + +Create a test namespace: + +```bash +kubectl create ns test +``` + +Create a Deployment named `podinfo`: + +```bash +kubectl apply -n test -f https://raw.githubusercontent.com/fluxcd/flagger/main/kustomize/podinfo/deployment.yaml +``` + +Create a ClusterIP Service and HTTPScaledObject which targets the `podinfo` deployment and uses HTTP Request as a trigger: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: podinfo + namespace: test + labels: + app: podinfo +spec: + type: ClusterIP + ports: + - port: 9898 + targetPort: http + protocol: TCP + name: http + selector: + app: podinfo +--- +apiVersion: http.keda.sh/v1alpha1 +kind: HTTPScaledObject +metadata: + name: podinfo + namespace: test + labels: + app: podinfo +spec: + hosts: + - www.example.com + pathPrefixes: + - / + replicas: + max: 10 + min: 0 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo + port: 9898 + service: podinfo + scalingMetric: + concurrency: + targetValue: 200 +``` + +Save the above resource as podinfo-canary-httpso.yaml and then apply it: + +```bash +kubectl apply -f ./podinfo-canary-httpso.yaml +``` + +Deploy the load testing service to generate traffic during the analysis: + +```bash +kubectl apply -k 'https://github.com/fluxcd/flagger//kustomize/tester?ref=main' +``` + +Create a canary custom resource \(replace "www.example.com" with your own domain\): + +```yaml +apiVersion: flagger.app/v1beta1 +kind: Canary +metadata: + name: podinfo + namespace: test +spec: + # deployment reference + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo + # HTTPScaledObject reference + autoscalerRef: + apiVersion: keda.sh/v1alpha1 + kind: HTTPScaledObject + name: podinfo + primaryScalingSet: + name: primary + kind: ClusterHTTPScalingSet + # the maximum time in seconds for the canary deployment + # to make progress before rollback (default 600s) + progressDeadlineSeconds: 60 + service: + # service port number + port: 8080 + # Reference to the Gateway that the generated HTTPRoute would attach to. + gatewayRefs: + - name: gateway + namespace: istio-ingress + analysis: + # schedule interval (default 60s) + interval: 1m + # max number of failed metric checks before rollback + threshold: 5 + # max traffic percentage routed to canary + # percentage (0-100) + maxWeight: 50 + # canary increment step + # percentage (0-100) + stepWeight: 10 + metrics: + - name: error-rate + # max error rate (5xx responses) + # percentage (0-100) + templateRef: + name: error-rate + namespace: flagger-system + thresholdRange: + max: 1 + interval: 1m + - name: latency + templateRef: + name: latency + namespace: flagger-system + # seconds + thresholdRange: + max: 0.5 + interval: 30s + # testing (optional) + webhooks: + - name: smoke-test + type: pre-rollout + url: http://flagger-loadtester.test/ + timeout: 15s + metadata: + type: bash + cmd: "curl -sd 'anon' -H 'Host: www.example.com' http://keda-http-add-on-interceptor-proxy.keda:8080/token | grep token" + - name: load-test + url: http://flagger-loadtester.test/ + timeout: 5s + metadata: + cmd: "hey -z 2m -q 10 -c 2 -host www.example.com http://gateway-istio.istio-ingress/" +``` + +Save the above resource as `podinfo-canary.yaml` and then apply it: + +```bash +kubectl apply -f ./podinfo-canary.yaml +``` + +After a couple of seconds Flagger will create the canary objects: + +```bash +# applied +deployment.apps/podinfo +service/podinfo +httpscaledobject.http.keda.sh/podinfo +canary.flagger.app/podinfo + +# generated +deployment.apps/podinfo-primary +service/podinfo-primary +httpscaledobject.http.keda.sh/podinfo-primary +httproutes.gateway.networking.k8s.io/podinfo +``` + +We refer to our HTTPScaledObject for the canary deployment using `.spec.autoscalerRef`. Flagger will use this to generate a primary HTTPScaledObject which will automatically scale their primary deployment up and down (including to/from zero) based on incoming HTTP traffic. + +In the situation when it is desired to have different scaling replica configuration between the canary and primary deployment HTTPScaledObject you can use +the `.spec.autoscalerRef.primaryScalerReplicas` to override these values for the generated primary HTTPScaledObject. + +After the boostrap, the podinfo deployment will be scaled to zero and the traffic to `podinfo.test` will be routed to the primary pods. +When the canary analysis starts, Flagger will call the pre-rollout webhooks before routing traffic to the canary. The Blue/Green deployment will run for five iterations while validating the HTTP metrics and rollout hooks every 15 seconds. + +## Expose the app outside the cluster + +Find the external address of Istio's load balancer: + +```bash +export ADDRESS="$(kubectl -n istio-ingress get svc/gateway-istio -ojson \ +| jq -r ".status.loadBalancer.ingress[].hostname")" +echo $ADDRESS +``` + +Configure your DNS server with a CNAME record \(AWS\) or A record \(GKE/AKS/DOKS\) and point a domain e.g. `www.example.com` to the LB address. + +Now you can access the podinfo UI using your domain address. + +Note that you should be using HTTPS when exposing production workloads on internet. You can obtain free TLS certs from Let's Encrypt, read this +[guide](https://github.com/stefanprodan/istio-gke) on how to configure cert-manager to secure Istio with TLS certificates. + +If you're using a local cluster via kind/k3s you can port forward the Envoy LoadBalancer service: + +```bash +kubectl port-forward -n istio-ingress svc/gateway-istio 8080:80 +``` + +Now you can access podinfo via `curl -H "Host: www.example.com" localhost:8080` + +## Automated canary promotion + +Trigger a canary deployment by updating the container image: + +```bash +kubectl -n test set image deployment/podinfo \ +podinfod=stefanprodan/podinfo:6.0.1 +``` + +Flagger detects that the deployment revision changed and starts a new rollout: + +```text +kubectl -n test describe canary/podinfo + +Status: + Canary Weight: 0 + Failed Checks: 0 + Phase: Succeeded +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal Synced 3m flagger New revision detected podinfo.test + Normal Synced 3m flagger Scaling up podinfo.test + Warning Synced 3m flagger Waiting for podinfo.test rollout to finish: 0 of 1 updated replicas are available + Normal Synced 3m flagger Advance podinfo.test canary weight 5 + Normal Synced 3m flagger Advance podinfo.test canary weight 10 + Normal Synced 3m flagger Advance podinfo.test canary weight 15 + Normal Synced 2m flagger Advance podinfo.test canary weight 20 + Normal Synced 2m flagger Advance podinfo.test canary weight 25 + Normal Synced 1m flagger Advance podinfo.test canary weight 30 + Normal Synced 1m flagger Advance podinfo.test canary weight 35 + Normal Synced 55s flagger Advance podinfo.test canary weight 40 + Normal Synced 45s flagger Advance podinfo.test canary weight 45 + Normal Synced 35s flagger Advance podinfo.test canary weight 50 + Normal Synced 25s flagger Copying podinfo.test template spec to podinfo-primary.test + Warning Synced 15s flagger Waiting for podinfo-primary.test rollout to finish: 1 of 2 updated replicas are available + Normal Synced 5s flagger Promotion completed! Scaling down podinfo.test +``` + +**Note** that if you apply new changes to the deployment during the canary analysis, Flagger will restart the analysis. + +A canary deployment is triggered by changes in any of the following objects: + +* Deployment PodSpec \(container image, command, ports, env, resources, etc\) +* ConfigMaps mounted as volumes or mapped to environment variables +* Secrets mounted as volumes or mapped to environment variables + +You can monitor how Flagger progressively changes the weights of the HTTPRoute object that is attahed to the Gateway with: + +```bash +watch kubectl get httproute -n test podinfo -o=jsonpath='{.spec.rules}' +``` + +You can monitor all canaries with: + +```bash +watch kubectl get canaries --all-namespaces + +NAMESPACE NAME STATUS WEIGHT LASTTRANSITIONTIME +test podinfo Progressing 15 2022-01-16T14:05:07Z +prod frontend Succeeded 0 2022-01-15T16:15:07Z +prod backend Failed 0 2022-01-14T17:05:07Z +``` + +## Automated rollback + +During the canary analysis you can generate HTTP 500 errors and high latency to test if Flagger pauses the rollout. + +Trigger another canary deployment: + +```bash +kubectl -n test set image deployment/podinfo \ +podinfod=stefanprodan/podinfo:6.0.2 +``` + +Exec into the load tester pod with: + +```bash +kubectl -n test exec -it flagger-loadtester-xx-xx sh +``` + +Generate HTTP 500 errors: + +```bash +watch curl -H 'Host: www.example.com' http://keda-http-add-on-interceptor-proxy.keda:8080/status/500 +``` + +Generate latency: + +```bash +watch curl -H 'Host: www.example.com' http://keda-http-add-on-interceptor-proxy.keda:8080/delay/1 +``` + +When the number of failed checks reaches the canary analysis threshold, the traffic is routed back to the primary, the canary is scaled to zero and the rollout is marked as failed. + +```text +kubectl -n test describe canary/podinfo + +Status: + Canary Weight: 0 + Failed Checks: 10 + Phase: Failed +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal Synced 3m flagger Starting canary deployment for podinfo.test + Normal Synced 3m flagger Advance podinfo.test canary weight 5 + Normal Synced 3m flagger Advance podinfo.test canary weight 10 + Normal Synced 3m flagger Advance podinfo.test canary weight 15 + Normal Synced 3m flagger Halt podinfo.test advancement error rate 69.17% > 1% + Normal Synced 2m flagger Halt podinfo.test advancement error rate 61.39% > 1% + Normal Synced 2m flagger Halt podinfo.test advancement error rate 55.06% > 1% + Normal Synced 2m flagger Halt podinfo.test advancement error rate 47.00% > 1% + Normal Synced 2m flagger (combined from similar events): Halt podinfo.test advancement error rate 38.08% > 1% + Warning Synced 1m flagger Rolling back podinfo.test failed checks threshold reached 10 + Warning Synced 1m flagger Canary failed! Scaling down podinfo.test +``` + +## Session Affinity + +While Flagger can perform weighted routing and A/B testing individually, with Gateway API it can combine the two leading to a Canary +release with session affinity. +For more information you can read the [deployment strategies docs](../usage/deployment-strategies.md#canary-release-with-session-affinity). + +> **Note:** The implementation must have support for the [`ResponseHeaderModifier`](https://github.com/kubernetes-sigs/gateway-api/blob/3d22aa5a08413222cb79e6b2e245870360434614/apis/v1beta1/httproute_types.go#L651) API. + +Create a canary custom resource \(replace with your own domain\): + +```yaml +apiVersion: flagger.app/v1beta1 +kind: Canary +metadata: + name: podinfo + namespace: test +spec: + # deployment reference + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo + # the maximum time in seconds for the canary deployment + # to make progress before it is rollback (default 600s) + progressDeadlineSeconds: 60 + # HTTPScaledObject reference + autoscalerRef: + apiVersion: keda.sh/v1alpha1 + kind: HTTPScaledObject + name: podinfo + primaryScalingSet: + name: primary + kind: ClusterHTTPScalingSet + service: + # service port number + port: 9898 + # Reference to the Gateway that the generated HTTPRoute would attach to. + gatewayRefs: + - name: gateway + namespace: istio-ingress + analysis: + # schedule interval (default 60s) + interval: 1m + # max number of failed metric checks before rollback + threshold: 5 + # max traffic percentage routed to canary + # percentage (0-100) + maxWeight: 50 + # canary increment step + # percentage (0-100) + stepWeight: 10 + # session affinity config + sessionAffinity: + # name of the cookie used + cookieName: flagger-cookie + # max age of the cookie (in seconds) + # optional; defaults to 86400 + maxAge: 21600 + metrics: + - name: error-rate + # max error rate (5xx responses) + # percentage (0-100) + templateRef: + name: error-rate + namespace: flagger-system + thresholdRange: + max: 1 + interval: 1m + - name: latency + templateRef: + name: latency + namespace: flagger-system + # seconds + thresholdRange: + max: 0.5 + interval: 30s + # testing (optional) + webhooks: + - name: smoke-test + type: pre-rollout + url: http://flagger-loadtester.test/ + timeout: 15s + metadata: + type: bash + cmd: "curl -sd 'anon' -H 'Host: www.example.com' http://keda-http-add-on-interceptor-proxy.keda:8080/token | grep token" + - name: load-test + url: http://flagger-loadtester.test/ + timeout: 5s + metadata: + cmd: "hey -z 2m -q 10 -c 2 -host www.example.com http://gateway-istio.istio-ingress/" +``` + +Save the above resource as podinfo-canary-session-affinity.yaml and then apply it: + +```bash +kubectl apply -f ./podinfo-canary-session-affinity.yaml +``` + +Trigger a canary deployment by updating the container image: + +```bash +kubectl -n test set image deployment/podinfo \ +podinfod=ghcr.io/stefanprodan/podinfo:6.0.1 +``` + +You can load `www.example.com` in your browser and refresh it until you see the requests being served by `podinfo:6.0.1`. +All subsequent requests after that will be served by `podinfo:6.0.1` and not `podinfo:6.0.0` because of the session affinity +configured by Flagger in the HTTPRoute object. + +# A/B Testing + +Besides weighted routing, Flagger can be configured to route traffic to the canary based on HTTP match conditions. In an A/B testing scenario, you'll be using HTTP headers or cookies to target a certain segment of your users. This is particularly useful for frontend applications that require session affinity. + +![Flagger A/B Testing Stages](https://raw.githubusercontent.com/fluxcd/flagger/main/docs/diagrams/flagger-abtest-steps.png) + +Create a canary custom resource \(replace "www.example.com" with your own domain\): + +```yaml +apiVersion: flagger.app/v1beta1 +kind: Canary +metadata: + name: podinfo + namespace: test +spec: + # deployment reference + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo + # the maximum time in seconds for the canary deployment + # to make progress before it is rollback (default 600s) + progressDeadlineSeconds: 60 + # HTTPScaledObject reference + autoscalerRef: + apiVersion: keda.sh/v1alpha1 + kind: HTTPScaledObject + name: podinfo + primaryScalingSet: + name: primary + kind: ClusterHTTPScalingSet + service: + # service port number + port: 9898 + # Reference to the Gateway that the generated HTTPRoute would attach to. + gatewayRefs: + - name: gateway + namespace: istio-ingress + analysis: + # schedule interval (default 60s) + interval: 1m + # max number of failed metric checks before rollback + threshold: 5 + # max traffic percentage routed to canary + # percentage (0-100) + maxWeight: 50 + # canary increment step + # percentage (0-100) + stepWeight: 10 + metrics: + - name: error-rate + # max error rate (5xx responses) + # percentage (0-100) + templateRef: + name: error-rate + namespace: flagger-system + thresholdRange: + max: 1 + interval: 1m + - name: latency + templateRef: + name: latency + namespace: flagger-system + # seconds + thresholdRange: + max: 0.5 + interval: 30s + # testing (optional) + webhooks: + - name: smoke-test + type: pre-rollout + url: http://flagger-loadtester.test/ + timeout: 15s + metadata: + type: bash + cmd: "curl -sd 'anon' -H 'Host: www.example.com' http://keda-http-add-on-interceptor-proxy.keda:8080/token | grep token" + - name: load-test + url: http://flagger-loadtester.test/ + timeout: 5s + metadata: + cmd: "hey -z 2m -q 10 -c 2 -host www.example.com -H 'X-Canary: insider' http://gateway-istio.istio-ingress/" +``` + +The above configuration will run an analysis for ten minutes targeting those users that have an insider cookie. + +Save the above resource as podinfo-ab-canary.yaml and then apply it: + +```bash +kubectl apply -f ./podinfo-ab-canary.yaml +``` + +Trigger a canary deployment by updating the container image: + +```bash +kubectl -n test set image deployment/podinfo \ +podinfod=stefanprodan/podinfo:6.0.3 +``` + +Flagger detects that the deployment revision changed and starts a new rollout: + +```text +kubectl -n test describe canary/podinfo + +Status: + Failed Checks: 0 + Phase: Succeeded +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal Synced 3m flagger New revision detected podinfo.test + Normal Synced 3m flagger Scaling up podinfo.test + Warning Synced 3m flagger Waiting for podinfo.test rollout to finish: 0 of 1 updated replicas are available + Normal Synced 3m flagger Advance podinfo.test canary iteration 1/10 + Normal Synced 3m flagger Advance podinfo.test canary iteration 2/10 + Normal Synced 3m flagger Advance podinfo.test canary iteration 3/10 + Normal Synced 2m flagger Advance podinfo.test canary iteration 4/10 + Normal Synced 2m flagger Advance podinfo.test canary iteration 5/10 + Normal Synced 1m flagger Advance podinfo.test canary iteration 6/10 + Normal Synced 1m flagger Advance podinfo.test canary iteration 7/10 + Normal Synced 55s flagger Advance podinfo.test canary iteration 8/10 + Normal Synced 45s flagger Advance podinfo.test canary iteration 9/10 + Normal Synced 35s flagger Advance podinfo.test canary iteration 10/10 + Normal Synced 25s flagger Copying podinfo.test template spec to podinfo-primary.test + Warning Synced 15s flagger Waiting for podinfo-primary.test rollout to finish: 1 of 2 updated replicas are available + Normal Synced 5s flagger Promotion completed! Scaling down podinfo.test +``` + +## Traffic mirroring + +![Flagger Canary Traffic Shadowing](https://raw.githubusercontent.com/fluxcd/flagger/main/docs/diagrams/flagger-canary-traffic-mirroring.png) + +For applications that perform read operations, Flagger can be configured to do B/G tests with traffic mirroring. +Gateway API traffic mirroring will copy each incoming request, sending one request to the primary and one to the canary service. +The response from the primary is sent back to the user and the response from the canary is discarded. +Metrics are collected on both requests so that the deployment will only proceed if the canary metrics are within the threshold values. + +Note that mirroring should be used for requests that are **idempotent** or capable of being processed twice \(once by the primary and once by the canary\). + +You can enable mirroring by replacing `stepWeight` with `iterations` and by setting `analysis.mirror` to `true`: + +```yaml +apiVersion: flagger.app/v1beta1 +kind: Canary +metadata: + name: podinfo + namespace: test +spec: + # deployment reference + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo + service: + # service port number + port: 9898 + # container port number or name (optional) + targetPort: 9898 + # Gateway API HTTPRoute host names + hosts: + - www.example.com + # Reference to the Gateway that the generated HTTPRoute would attach to. + gatewayRefs: + - name: gateway + namespace: istio-ingress + analysis: + # schedule interval + interval: 1m + # max number of failed metric checks before rollback + threshold: 5 + # total number of iterations + iterations: 10 + # enable traffic shadowing + mirror: true + # Gateway API HTTPRoute host names + metrics: + - name: request-success-rate + thresholdRange: + min: 99 + interval: 1m + - name: request-duration + thresholdRange: + max: 500 + interval: 1m + webhooks: + - name: load-test + url: http://flagger-loadtester.test/ + timeout: 5s + metadata: + cmd: "hey -z 2m -q 10 -c 2 -host www.example.com http://gateway-istio.istio-ingress/" +``` + +With the above configuration, Flagger will run a canary release with the following steps: + +* detect new revision \(deployment spec, secrets or configmaps changes\) +* scale from zero the canary deployment +* wait for the HPA to set the canary minimum replicas +* check canary pods health +* run the acceptance tests +* abort the canary release if tests fail +* start the load tests +* mirror 100% of the traffic from primary to canary +* check request success rate and request duration every minute +* abort the canary release if the metrics check failure threshold is reached +* stop traffic mirroring after the number of iterations is reached +* route live traffic to the canary pods +* promote the canary \(update the primary secrets, configmaps and deployment spec\) +* wait for the primary deployment rollout to finish +* wait for the HPA to set the primary minimum replicas +* check primary pods health +* switch live traffic back to primary +* scale to zero the canary +* send notification with the canary analysis result + +The above procedures can be extended with [custom metrics](../usage/metrics.md) checks, [webhooks](../usage/webhooks.md), [manual promotion](../usage/webhooks.md#manual-gating) approval and [Slack or MS Teams](../usage/alerting.md) notifications. diff --git a/kustomize/base/flagger/crd.yaml b/kustomize/base/flagger/crd.yaml index 39a466dd2..f749244c3 100644 --- a/kustomize/base/flagger/crd.yaml +++ b/kustomize/base/flagger/crd.yaml @@ -119,6 +119,7 @@ spec: enum: - HorizontalPodAutoscaler - ScaledObject + - HTTPScaledObject name: type: string primaryScalerQueries: @@ -134,6 +135,38 @@ spec: maxReplicas: type: integer minimum: 1 + canaryInterceptorProxyService: + type: object + description: Specify this service if you want to change the Canary interceptor proxy service from its default value. + properties: + name: + default: keda-http-add-on-interceptor-proxy + maxLength: 253 + minLength: 1 + type: string + namespace: + default: keda + maxLength: 63 + minLength: 1 + type: string + primaryScalingSet: + type: object + description: |- + PrimaryScalingSet to be used for primary HTTPScaledObject, if empty, default interceptor and scaler will be used. + properties: + kind: + description: Kind of the resource being referred to. Defaults to HTTPScalingSet. + enum: + - HTTPScalingSet + - ClusterHTTPScalingSet + type: string + name: + description: Name of the scaling set + type: string + namespace: + maxLength: 63 + minLength: 1 + type: string ingressRef: description: Ingress selector type: object diff --git a/kustomize/base/flagger/rbac.yaml b/kustomize/base/flagger/rbac.yaml index 7e46cd99c..9b7c1e210 100644 --- a/kustomize/base/flagger/rbac.yaml +++ b/kustomize/base/flagger/rbac.yaml @@ -229,6 +229,19 @@ rules: - update - patch - delete + - apiGroups: + - http.keda.sh + resources: + - httpscaledobjects + - httpscaledobjects/finalizers + verbs: + - get + - list + - watch + - create + - update + - patch + - delete - apiGroups: - apisix.apache.org resources: diff --git a/pkg/apis/flagger/v1beta1/canary.go b/pkg/apis/flagger/v1beta1/canary.go index 8c5ac5393..8abd4cadc 100644 --- a/pkg/apis/flagger/v1beta1/canary.go +++ b/pkg/apis/flagger/v1beta1/canary.go @@ -21,6 +21,7 @@ import ( "time" "github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1beta1" + http "github.com/fluxcd/flagger/pkg/apis/http/v1alpha1" istiov1beta1 "github.com/fluxcd/flagger/pkg/apis/istio/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" @@ -458,6 +459,35 @@ type LocalObjectReference struct { Name string `json:"name"` } +// CanaryInterceptorProxyService specifies the service if you want to change +// the Canary interceptor proxy service from its default value. +type CanaryInterceptorProxyService struct { + // Name of the canary interceptor proxy service. + // Defaults to "keda-http-add-on-interceptor-proxy". + // +optional + Name string `json:"name,omitempty"` + + // Namespace of the canary interceptor proxy service. + // Defaults to "keda". + // +optional + Namespace string `json:"namespace,omitempty"` +} + +// PrimaryScalingSet defines the desired scaling set to be used +type PrimaryScalingSet struct { + // Kind of the resource being referred to. Defaults to HTTPScalingSet. + // +optional + Kind http.ScalingSetKind `json:"kind,omitempty"` + + // Name of the scaling set + Name string `json:"name,omitempty"` + + // Namespace of the scaling set + // Defaults to "keda". + // +optional + Namespace string `json:"namespace,omitempty"` +} + type AutoscalerReference struct { // API version of the scaler // +required @@ -480,6 +510,16 @@ type AutoscalerReference struct { // autoscaler replicas. // +optional PrimaryScalerReplicas *ScalerReplicas `json:"primaryScalerReplicas,omitempty"` + + // CanaryInterceptorProxyService specifies the service if you want to change + // the Canary interceptor proxy service from its default value. + // +optional + CanaryInterceptorProxyService *CanaryInterceptorProxyService `json:"canaryInterceptorProxyService,omitempty"` + + // PrimaryScalingSet is the scaling set to be used for the primary + // scaler, if a scaler supports scaling using queries. + // +optional + PrimaryScalingSet *PrimaryScalingSet `json:"primaryScalingSet,omitempty"` } // ScalerReplicas holds overrides for autoscaler replicas @@ -624,3 +664,8 @@ func (c *Canary) SkipAnalysis() bool { } return c.Spec.SkipAnalysis } + +// IsHTTPScaledObject returns true if the autoscalerRef is a HTTPScaledObject +func (c *Canary) IsHTTPScaledObject() bool { + return c.Spec.AutoscalerRef != nil && c.Spec.AutoscalerRef.Kind == "HTTPScaledObject" +} diff --git a/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go b/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go index 340fc0439..25651167e 100644 --- a/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go @@ -166,6 +166,16 @@ func (in *AutoscalerRefernce) DeepCopyInto(out *AutoscalerRefernce) { *out = new(ScalerReplicas) (*in).DeepCopyInto(*out) } + if in.CanaryInterceptorProxyService != nil { + in, out := &in.CanaryInterceptorProxyService, &out.CanaryInterceptorProxyService + *out = new(CanaryInterceptorProxyService) + **out = **in + } + if in.PrimaryScalingSet != nil { + in, out := &in.PrimaryScalingSet, &out.PrimaryScalingSet + *out = new(PrimaryScalingSet) + **out = **in + } return } @@ -304,6 +314,22 @@ func (in *CanaryCondition) DeepCopy() *CanaryCondition { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CanaryInterceptorProxyService) DeepCopyInto(out *CanaryInterceptorProxyService) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CanaryInterceptorProxyService. +func (in *CanaryInterceptorProxyService) DeepCopy() *CanaryInterceptorProxyService { + if in == nil { + return nil + } + out := new(CanaryInterceptorProxyService) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CanaryList) DeepCopyInto(out *CanaryList) { *out = *in @@ -868,6 +894,22 @@ func (in *MetricTemplateStatus) DeepCopy() *MetricTemplateStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PrimaryScalingSet) DeepCopyInto(out *PrimaryScalingSet) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrimaryScalingSet. +func (in *PrimaryScalingSet) DeepCopy() *PrimaryScalingSet { + if in == nil { + return nil + } + out := new(PrimaryScalingSet) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ScalerReplicas) DeepCopyInto(out *ScalerReplicas) { *out = *in diff --git a/pkg/apis/http/register.go b/pkg/apis/http/register.go new file mode 100644 index 000000000..5ee2620f0 --- /dev/null +++ b/pkg/apis/http/register.go @@ -0,0 +1,5 @@ +package http + +const ( + GroupName = "http.keda.sh" +) diff --git a/pkg/apis/http/v1alpha1/condition_types.go b/pkg/apis/http/v1alpha1/condition_types.go new file mode 100644 index 000000000..114265d37 --- /dev/null +++ b/pkg/apis/http/v1alpha1/condition_types.go @@ -0,0 +1,88 @@ +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// +kubebuilder:validation:Enum=Ready + +// HTTPScaledObjectCreationStatus describes the creation status +// of the scaler's additional resources such as Services, Ingresses and Deployments +type HTTPScaledObjectCreationStatus string + +const ( + // Ready indicates the object is fully created + Ready HTTPScaledObjectCreationStatus = "Ready" +) + +// +kubebuilder:validation:Enum=ErrorCreatingAppScaledObject;AppScaledObjectCreated;TerminatingResources;AppScaledObjectTerminated;AppScaledObjectTerminationError;PendingCreation;HTTPScaledObjectIsReady; + +// HTTPScaledObjectConditionReason describes the reason why the condition transitioned +type HTTPScaledObjectConditionReason string + +const ( + ErrorCreatingAppScaledObject HTTPScaledObjectConditionReason = "ErrorCreatingAppScaledObject" + AppScaledObjectCreated HTTPScaledObjectConditionReason = "AppScaledObjectCreated" + TerminatingResources HTTPScaledObjectConditionReason = "TerminatingResources" + AppScaledObjectTerminated HTTPScaledObjectConditionReason = "AppScaledObjectTerminated" + AppScaledObjectTerminationError HTTPScaledObjectConditionReason = "AppScaledObjectTerminationError" + PendingCreation HTTPScaledObjectConditionReason = "PendingCreation" + HTTPScaledObjectIsReady HTTPScaledObjectConditionReason = "HTTPScaledObjectIsReady" +) + +// HTTPScaledObjectCondition stores the condition state +type HTTPScaledObjectCondition struct { + // Timestamp of the condition + // +optional + Timestamp string `json:"timestamp" description:"Timestamp of this condition"` + // Type of condition + // +required + Type HTTPScaledObjectCreationStatus `json:"type" description:"type of status condition"` + // Status of the condition, one of True, False, Unknown. + // +required + Status metav1.ConditionStatus `json:"status" description:"status of the condition, one of True, False, Unknown"` + // Reason for the condition's last transition. + // +optional + Reason HTTPScaledObjectConditionReason `json:"reason,omitempty" description:"one-word CamelCase reason for the condition's last transition"` + // Message indicating details about the transition. + // +optional + Message string `json:"message,omitempty" description:"human-readable message indicating details about last transition"` +} + +type Conditions []HTTPScaledObjectCondition + +// GetReadyCondition returns Condition of type Ready +func (c *Conditions) GetReadyCondition() HTTPScaledObjectCondition { + if *c == nil { + c = GetInitializedConditions() + } + return c.getCondition(Ready) +} + +// GetInitializedConditions returns Conditions initialized to the default -> Status: Unknown +func GetInitializedConditions() *Conditions { + return &Conditions{{Type: Ready, Status: metav1.ConditionUnknown}} +} + +// IsTrue is true if the condition is True +func (c *HTTPScaledObjectCondition) IsTrue() bool { + if c == nil { + return false + } + return c.Status == metav1.ConditionTrue +} + +// IsFalse is true if the condition is False +func (c *HTTPScaledObjectCondition) IsFalse() bool { + if c == nil { + return false + } + return c.Status == metav1.ConditionFalse +} + +func (c Conditions) getCondition(conditionType HTTPScaledObjectCreationStatus) HTTPScaledObjectCondition { + for i := range c { + if c[i].Type == conditionType { + return c[i] + } + } + return HTTPScaledObjectCondition{} +} diff --git a/pkg/apis/http/v1alpha1/doc.go b/pkg/apis/http/v1alpha1/doc.go new file mode 100644 index 000000000..47b747cf1 --- /dev/null +++ b/pkg/apis/http/v1alpha1/doc.go @@ -0,0 +1,5 @@ +// +k8s:deepcopy-gen=package + +// Package v1 is the v1 version of the API. +// +groupName=http.keda.sh +package v1alpha1 diff --git a/pkg/apis/http/v1alpha1/httpscaledobject_types.go b/pkg/apis/http/v1alpha1/httpscaledobject_types.go new file mode 100644 index 000000000..f1f61b04f --- /dev/null +++ b/pkg/apis/http/v1alpha1/httpscaledobject_types.go @@ -0,0 +1,183 @@ +/* +Copyright 2023 The KEDA Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ScalingSetKind string + +const ( + HTTPScalingSetKind ScalingSetKind = "HTTPScalingSet" + ClusterHTTPScalingSetKind ScalingSetKind = "ClusterHTTPScalingSet" +) + +// ScaleTargetRef contains all the details about an HTTP application to scale and route to +type ScaleTargetRef struct { + // +optional + Name string `json:"name"` + // +optional + APIVersion string `json:"apiVersion,omitempty"` + // +optional + Kind string `json:"kind,omitempty"` + // The name of the service to route to + Service string `json:"service"` + // The port to route to + Port int32 `json:"port"` +} + +// ReplicaStruct contains the minimum and maximum amount of replicas to have in the deployment +type ReplicaStruct struct { + // Minimum amount of replicas to have in the deployment (Default 0) + Min *int32 `json:"min,omitempty" description:"Minimum amount of replicas to have in the deployment (Default 0)"` + // Maximum amount of replicas to have in the deployment (Default 100) + Max *int32 `json:"max,omitempty" description:"Maximum amount of replicas to have in the deployment (Default 100)"` +} + +// ScalingMetricSpec contains the scaling calculation type +type ScalingMetricSpec struct { + // Scaling based on concurrent requests for a given target + Concurrency *ConcurrencyMetricSpec `json:"concurrency,omitempty" description:"Scaling based on concurrent requests for a given target. 'concurrency' and 'rate' are mutually exclusive."` + // Scaling based the average rate during an specific time window for a given target + Rate *RateMetricSpec `json:"requestRate,omitempty" description:"Scaling based the average rate during an specific time window for a given target. 'concurrency' and 'rate' are mutually exclusive."` +} + +// ConcurrencyMetricSpec defines the concurrency scaling +type ConcurrencyMetricSpec struct { + // Target value for rate scaling + // +kubebuilder:default=100 + // +optional + TargetValue int `json:"targetValue" description:"Target value for concurrency scaling"` +} + +// RateMetricSpec defines the concurrency scaling +type RateMetricSpec struct { + // Target value for rate scaling + // +kubebuilder:default=100 + // +optional + TargetValue int `json:"targetValue" description:"Target value for rate scaling"` + // Time window for rate calculation + // +kubebuilder:default="1m" + // +optional + Window metav1.Duration `json:"window" description:"Time window for rate calculation"` + // Time granularity for rate calculation + // +kubebuilder:default="1s" + // +optional + Granularity metav1.Duration `json:"granularity" description:"Time granularity for rate calculation"` +} + +// HTTPSalingSetTargetRef defines the desired scaling set to be used +type HTTPSalingSetTargetRef struct { + // Name of the scaling set + Name string `json:"name,omitempty"` + // Kind of the resource being referred to. Defaults to HTTPScalingSet. + // +kubebuilder:validation:Enum=HTTPScalingSet;ClusterHTTPScalingSet + // +optional + Kind ScalingSetKind `json:"kind,omitempty"` +} + +func (so *HTTPScaledObjectSpec) GetHTTPSalingSetTargetRef() HTTPSalingSetTargetRef { + r := HTTPSalingSetTargetRef{} + if so.ScalingSet == nil { + return r + } + + r.Name = so.ScalingSet.Name + r.Kind = ClusterHTTPScalingSetKind + if so.ScalingSet.Kind != "" { + r.Kind = so.ScalingSet.Kind + } + return r +} + +// HTTPScaledObjectSpec defines the desired state of HTTPScaledObject +type HTTPScaledObjectSpec struct { + // ScalingSet to be used for this HTTPScaledObject, if empty, default + // interceptor and scaler will be used + // +optional + ScalingSet *HTTPSalingSetTargetRef `json:"scalingSet,omitempty"` + // The hosts to route. All requests which the "Host" header + // matches any .spec.hosts (and the Request Target matches any + // .spec.pathPrefixes) will be routed to the Service and Port specified in + // the scaleTargetRef. + Hosts []string `json:"hosts,omitempty"` + // The paths to route. All requests which the Request Target matches any + // .spec.pathPrefixes (and the "Host" header matches any .spec.hosts) + // will be routed to the Service and Port specified in + // the scaleTargetRef. + // +optional + PathPrefixes []string `json:"pathPrefixes,omitempty"` + // The name of the deployment to route HTTP requests to (and to autoscale). + ScaleTargetRef ScaleTargetRef `json:"scaleTargetRef"` + // (optional) Replica information + // +optional + Replicas *ReplicaStruct `json:"replicas,omitempty"` + // (optional) DEPRECATED (use ScalingMetric instead) Target metric value + // +optional + TargetPendingRequests *int32 `json:"targetPendingRequests,omitempty" description:"The target metric value for the HPA (Default 100)"` + // (optional) Cooldown period value + // +optional + CooldownPeriod *int32 `json:"scaledownPeriod,omitempty" description:"Cooldown period (seconds) for resources to scale down (Default 300)"` + // (optional) Configuration for the metric used for scaling + // +optional + ScalingMetric *ScalingMetricSpec `json:"scalingMetric,omitempty" description:"Configuration for the metric used for scaling. If empty 'concurrency' will be used"` +} + +// HTTPScaledObjectStatus defines the observed state of HTTPScaledObject +type HTTPScaledObjectStatus struct { + // TargetWorkload reflects details about the scaled workload. + // +optional + TargetWorkload string `json:"targetWorkload,omitempty" description:"It reflects details about the scaled workload"` + // TargetService reflects details about the scaled service. + // +optional + TargetService string `json:"targetService,omitempty" description:"It reflects details about the scaled service"` + // Conditions of the operator + Conditions Conditions `json:"conditions,omitempty" description:"List of auditable conditions of the operator"` +} + +// +genclient +// +k8s:openapi-gen=true +// +kubebuilder:object:root=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:printcolumn:name="TargetWorkload",type="string",JSONPath=".status.targetWorkload" +// +kubebuilder:printcolumn:name="TargetService",type="string",JSONPath=".status.targetService" +// +kubebuilder:printcolumn:name="MinReplicas",type="integer",JSONPath=".spec.replicas.min" +// +kubebuilder:printcolumn:name="MaxReplicas",type="integer",JSONPath=".spec.replicas.max" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:printcolumn:name="Active",type="string",JSONPath=".status.conditions[?(@.type==\"HTTPScaledObjectIsReady\")].status" +// +kubebuilder:resource:shortName=httpso +// +kubebuilder:subresource:status + +// HTTPScaledObject is the Schema for the httpscaledobjects API +type HTTPScaledObject struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec HTTPScaledObjectSpec `json:"spec"` + Status HTTPScaledObjectStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// HTTPScaledObjectList contains a list of HTTPScaledObject +type HTTPScaledObjectList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []HTTPScaledObject `json:"items"` +} diff --git a/pkg/apis/http/v1alpha1/httpscalingset_defaults.go b/pkg/apis/http/v1alpha1/httpscalingset_defaults.go new file mode 100644 index 000000000..0f8a4f6e1 --- /dev/null +++ b/pkg/apis/http/v1alpha1/httpscalingset_defaults.go @@ -0,0 +1,149 @@ +/* +Copyright 2024 The KEDA Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "fmt" +) + +var ( + interceptorImage = fmt.Sprintf("ghcr.io/kedacore/http-add-on-interceptor:%s", Version()) + scalerImage = fmt.Sprintf("ghcr.io/kedacore/http-add-on-scaler:%s", Version()) +) + +func (c *HTTPInterceptorSpec) GetProxyPort() int32 { + if c.Config == nil || c.Config.ProxyPort == nil { + return 8080 + } + return *c.Config.ProxyPort +} + +func (c *HTTPInterceptorSpec) GetAdminPort() int32 { + if c.Config == nil || c.Config.AdminPort == nil { + return 9090 + } + return *c.Config.AdminPort +} +func (c *HTTPInterceptorSpec) GetConnectTimeout() string { + if c.Config == nil || c.Config.ConnectTimeout == nil { + return "500ms" + } + return *c.Config.ConnectTimeout +} +func (c *HTTPInterceptorSpec) GetHeaderTimeout() string { + if c.Config == nil || c.Config.HeaderTimeout == nil { + return "500ms" + } + return *c.Config.HeaderTimeout +} +func (c *HTTPInterceptorSpec) GetWaitTimeout() string { + if c.Config == nil || c.Config.WaitTimeout == nil { + return "1500ms" + } + return *c.Config.WaitTimeout +} +func (c *HTTPInterceptorSpec) GetIdleConnTimeout() string { + if c.Config == nil || c.Config.IdleConnTimeout == nil { + return "90s" + } + return *c.Config.IdleConnTimeout +} +func (c *HTTPInterceptorSpec) GetTLSHandshakeTimeout() string { + if c.Config == nil || c.Config.TLSHandshakeTimeout == nil { + return "10s" + } + return *c.Config.TLSHandshakeTimeout +} +func (c *HTTPInterceptorSpec) GetExpectContinueTimeout() string { + if c.Config == nil || c.Config.ExpectContinueTimeout == nil { + return "1s" + } + return *c.Config.ExpectContinueTimeout +} +func (c *HTTPInterceptorSpec) GetForceHTTP2() bool { + if c.Config == nil || c.Config.ForceHTTP2 == nil { + return false + } + return *c.Config.ForceHTTP2 +} +func (c *HTTPInterceptorSpec) GetKeepAlive() string { + if c.Config == nil || c.Config.KeepAlive == nil { + return "1s" + } + return *c.Config.KeepAlive +} +func (c *HTTPInterceptorSpec) GetMaxIdleConns() int { + if c.Config == nil || c.Config.MaxIdleConns == nil { + return 100 + } + return *c.Config.MaxIdleConns +} +func (c *HTTPInterceptorSpec) GetPollingInterval() int { + if c.Config == nil || c.Config.PollingInterval == nil { + return 1000 + } + return *c.Config.PollingInterval +} + +func (c *HTTPInterceptorSpec) GetImage() string { + if c.Image == nil { + return interceptorImage + } + return *c.Image +} + +func (c *HTTPInterceptorSpec) GetLabels() map[string]string { + if c.Labels == nil { + return map[string]string{} + } + return c.Labels +} + +func (c *HTTPInterceptorSpec) GetAnnotations() map[string]string { + if c.Annotations == nil { + return map[string]string{} + } + return c.Annotations +} + +func (c *HTTPScalerSpec) GetPort() int32 { + if c.Config.Port == nil { + return 9090 + } + return *c.Config.Port +} + +func (c *HTTPScalerSpec) GetImage() string { + if c.Image == nil { + return scalerImage + } + return *c.Image +} + +func (c *HTTPScalerSpec) GetLabels() map[string]string { + if c.Labels == nil { + return map[string]string{} + } + return c.Labels +} + +func (c *HTTPScalerSpec) GetAnnotations() map[string]string { + if c.Annotations == nil { + return map[string]string{} + } + return c.Annotations +} diff --git a/pkg/apis/http/v1alpha1/httpscalingset_types.go b/pkg/apis/http/v1alpha1/httpscalingset_types.go new file mode 100644 index 000000000..bedf621cd --- /dev/null +++ b/pkg/apis/http/v1alpha1/httpscalingset_types.go @@ -0,0 +1,233 @@ +/* +Copyright 2024 The KEDA Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// HTTPInterceptorScalingSpec defines the desired state of Interceptor autoscaling +type HTTPInterceptorScalingSpec struct { + // +kubebuilder:default=3 + // +optional + // Minimum replicas for the interceptor + MinReplicas int `json:"minReplicas"` + // +kubebuilder:default=100 + // +optional + // Maximum replicas for the interceptor + MaxReplicas int `json:"maxReplicas"` + // +kubebuilder:default=100 + // +optional + // Target concurrent requests + Target int `json:"target"` +} + +// HTTPInterceptorConfigurationSpec defines the desired state of Interceptor configuration +type HTTPInterceptorConfigurationSpec struct { + // +optional + // Port to be used for proxy operations + ProxyPort *int32 `json:"proxyPort,omitempty"` + // +optional + // Port to be used for admin operations + AdminPort *int32 `json:"adminPort,omitempty"` + // +optional + // Timeout for establishing the connection + ConnectTimeout *string `json:"connectTimeout,omitempty"` + // +optional + // How long to wait between when the HTTP request + // is sent to the backing app and when response headers need to arrive + HeaderTimeout *string `json:"headerTimeout,omitempty"` + // +optional + // How long to wait for the backing workload + // to have 1 or more replicas before connecting and sending the HTTP request. + WaitTimeout *string `json:"waitTimeout,omitempty"` + // +optional + // Timeout after which a connection in the interceptor's + // internal connection pool will be closed + IdleConnTimeout *string `json:"idleConnTimeout,omitempty"` + // +optional + // Max amount of time the interceptor will + // wait to establish a TLS connection + TLSHandshakeTimeout *string `json:"handshakeTimeout,omitempty"` + // +optional + // Max amount of time the interceptor will wait + // after sending request headers if the server returned an Expect: 100-continue + // header + ExpectContinueTimeout *string `json:"expectContinueTimeout,omitempty"` + // +optional + // Try to force HTTP2 for all requests + ForceHTTP2 *bool `json:"forceHTTP2,omitempty"` + // +optional + // Interval between keepalive probes + KeepAlive *string `json:"keepAlive,omitempty"` + // +optional + // Max number of connections that can be idle in the + // interceptor's internal connection pool + MaxIdleConns *int `json:"maxIdleConnections,omitempty"` + // +optional + // The interceptor has an internal process that periodically fetches the state + // of endpoints that is running the servers it forwards to. + // This is the interval (in milliseconds) representing how often to do a fetch + PollingInterval *int `json:"pollingInterval,omitempty"` +} + +// HTTPInterceptorSpec defines the desired state of Interceptor component +type HTTPInterceptorSpec struct { + // +optional + // Traffic configuration + Config *HTTPInterceptorConfigurationSpec `json:"config,omitempty"` + // Number of replicas for the interceptor + Replicas *int32 `json:"replicas,omitempty"` + // Container image name. + // +optional + Image *string `json:"image,omitempty"` + // ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this PodSpec. + // If specified, these secrets will be passed to individual puller implementations for them to use. + // More info: https://kubernetes.io/docs/concepts/containers/images#specifying-imagepullsecrets-on-a-pod + // +optional + ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` + // Autoscaling options for the interceptor + Autoscaling *HTTPInterceptorScalingSpec `json:"autoscaling,omitempty"` + // Compute Resources required by this interceptor. + // Cannot be updated. + // More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + // +optional + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + // Map of string keys and values that can be used to organize and categorize + // (scope and select) objects. May match selectors of replication controllers + // and services. + // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels + // +optional + Labels map[string]string `json:"labels,omitempty"` + // Annotations is an unstructured key value map stored with a resource that may be + // set by external tools to store and retrieve arbitrary metadata. They are not + // queryable and should be preserved when modifying objects. + // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations + // +optional + Annotations map[string]string `json:"annotations,omitempty"` + // +kubebuilder:default=default + // +optional + // Name of the service account to be used + ServiceAccountName string `json:"serviceAccountName"` +} + +// HTTPScalerConfigurationSpec defines the desired state of scaler configuration +type HTTPScalerConfigurationSpec struct { + // +kubebuilder:default=9090 + // +optional + // Port to be used for proxy operations + Port *int32 `json:"port,omitempty"` +} + +// HTTPScalerSpec defines the desired state of Scaler component +type HTTPScalerSpec struct { + // +kubebuilder:default={} + // +optional + // Traffic configuration + Config HTTPScalerConfigurationSpec `json:"config,omitempty"` + // Number of replicas for the interceptor + Replicas *int32 `json:"replicas,omitempty"` + // Container image name. + // +optional + Image *string `json:"image,omitempty"` + // ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this PodSpec. + // If specified, these secrets will be passed to individual puller implementations for them to use. + // More info: https://kubernetes.io/docs/concepts/containers/images#specifying-imagepullsecrets-on-a-pod + // +optional + ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` + // Compute Resources required by this scaler. + // Cannot be updated. + // More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + // +optional + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + // Map of string keys and values that can be used to organize and categorize + // (scope and select) objects. May match selectors of replication controllers + // and services. + // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels + // +optional + Labels map[string]string `json:"labels,omitempty"` + // Annotations is an unstructured key value map stored with a resource that may be + // set by external tools to store and retrieve arbitrary metadata. They are not + // queryable and should be preserved when modifying objects. + // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations + // +optional + Annotations map[string]string `json:"annotations,omitempty"` + // +kubebuilder:default=default + // +optional + // Name of the service account to be used + ServiceAccountName string `json:"serviceAccountName"` +} + +// HTTPScalingSetSpec defines the desired state of HTTPScalingSet +type HTTPScalingSetSpec struct { + Interceptor HTTPInterceptorSpec `json:"interceptor"` + Scaler HTTPScalerSpec `json:"scaler"` +} + +// HTTPScalingSetStatus defines the observed state of HTTPScalingSet +type HTTPScalingSetStatus struct{} + +// +genclient +// +k8s:openapi-gen=true +// +kubebuilder:object:root=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:shortName=ss +// +kubebuilder:subresource:status + +// HTTPScalingSet is the Schema for the httpscalingset API +type HTTPScalingSet struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec HTTPScalingSetSpec `json:"spec,omitempty"` + Status HTTPScalingSetStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// HTTPScalingSetList contains a list of HTTPScalingSetList +type HTTPScalingSetList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []HTTPScalingSet `json:"items"` +} + +// +genclient +// +k8s:openapi-gen=true +// +kubebuilder:object:root=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:shortName=css,scope=Cluster +// +kubebuilder:subresource:status + +// ClusterHTTPScalingSet is the Schema for the cluster httpscalingset API +type ClusterHTTPScalingSet struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec HTTPScalingSetSpec `json:"spec,omitempty"` + Status HTTPScalingSetStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// ClusterHTTPScalingSetList contains a list of ClusterHTTPScalingSet +type ClusterHTTPScalingSetList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ClusterHTTPScalingSet `json:"items"` +} diff --git a/pkg/apis/http/v1alpha1/register.go b/pkg/apis/http/v1alpha1/register.go new file mode 100644 index 000000000..6af69e227 --- /dev/null +++ b/pkg/apis/http/v1alpha1/register.go @@ -0,0 +1,56 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "github.com/fluxcd/flagger/pkg/apis/http" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: http.GroupName, Version: "v1alpha1"} + +// Kind takes an unqualified kind and returns back a Group qualified GroupKind +func Kind(kind string) schema.GroupKind { + return SchemeGroupVersion.WithKind(kind).GroupKind() +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + AddToScheme = SchemeBuilder.AddToScheme +) + +// Adds the list of known types to Scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &HTTPScaledObject{}, + &HTTPScaledObjectList{}, + &HTTPScalingSet{}, + &HTTPScalingSetList{}, + &ClusterHTTPScalingSet{}, + &ClusterHTTPScalingSetList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/pkg/apis/http/v1alpha1/version.go b/pkg/apis/http/v1alpha1/version.go new file mode 100644 index 000000000..af463fcc2 --- /dev/null +++ b/pkg/apis/http/v1alpha1/version.go @@ -0,0 +1,30 @@ +package v1alpha1 + +import ( + "fmt" + "runtime" + + "github.com/go-logr/logr" +) + +var ( + version = "main" + gitCommit string +) + +// Version returns the current git SHA of commit the binary was built from +func Version() string { + return version +} + +// GitCommit stores the current commit hash +func GitCommit() string { + return gitCommit +} + +func PrintComponentInfo(logger logr.Logger, component string) { + logger.Info(fmt.Sprintf("%s Version: %s", component, Version())) + logger.Info(fmt.Sprintf("%s Commit: %s", component, GitCommit())) + logger.Info(fmt.Sprintf("Go Version: %s", runtime.Version())) + logger.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH)) +} diff --git a/pkg/apis/http/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/http/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 000000000..35358fa54 --- /dev/null +++ b/pkg/apis/http/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,687 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/api/core/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterHTTPScalingSet) DeepCopyInto(out *ClusterHTTPScalingSet) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterHTTPScalingSet. +func (in *ClusterHTTPScalingSet) DeepCopy() *ClusterHTTPScalingSet { + if in == nil { + return nil + } + out := new(ClusterHTTPScalingSet) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterHTTPScalingSet) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterHTTPScalingSetList) DeepCopyInto(out *ClusterHTTPScalingSetList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterHTTPScalingSet, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterHTTPScalingSetList. +func (in *ClusterHTTPScalingSetList) DeepCopy() *ClusterHTTPScalingSetList { + if in == nil { + return nil + } + out := new(ClusterHTTPScalingSetList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterHTTPScalingSetList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConcurrencyMetricSpec) DeepCopyInto(out *ConcurrencyMetricSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConcurrencyMetricSpec. +func (in *ConcurrencyMetricSpec) DeepCopy() *ConcurrencyMetricSpec { + if in == nil { + return nil + } + out := new(ConcurrencyMetricSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in Conditions) DeepCopyInto(out *Conditions) { + { + in := &in + *out = make(Conditions, len(*in)) + copy(*out, *in) + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Conditions. +func (in Conditions) DeepCopy() Conditions { + if in == nil { + return nil + } + out := new(Conditions) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPInterceptorConfigurationSpec) DeepCopyInto(out *HTTPInterceptorConfigurationSpec) { + *out = *in + if in.ProxyPort != nil { + in, out := &in.ProxyPort, &out.ProxyPort + *out = new(int32) + **out = **in + } + if in.AdminPort != nil { + in, out := &in.AdminPort, &out.AdminPort + *out = new(int32) + **out = **in + } + if in.ConnectTimeout != nil { + in, out := &in.ConnectTimeout, &out.ConnectTimeout + *out = new(string) + **out = **in + } + if in.HeaderTimeout != nil { + in, out := &in.HeaderTimeout, &out.HeaderTimeout + *out = new(string) + **out = **in + } + if in.WaitTimeout != nil { + in, out := &in.WaitTimeout, &out.WaitTimeout + *out = new(string) + **out = **in + } + if in.IdleConnTimeout != nil { + in, out := &in.IdleConnTimeout, &out.IdleConnTimeout + *out = new(string) + **out = **in + } + if in.TLSHandshakeTimeout != nil { + in, out := &in.TLSHandshakeTimeout, &out.TLSHandshakeTimeout + *out = new(string) + **out = **in + } + if in.ExpectContinueTimeout != nil { + in, out := &in.ExpectContinueTimeout, &out.ExpectContinueTimeout + *out = new(string) + **out = **in + } + if in.ForceHTTP2 != nil { + in, out := &in.ForceHTTP2, &out.ForceHTTP2 + *out = new(bool) + **out = **in + } + if in.KeepAlive != nil { + in, out := &in.KeepAlive, &out.KeepAlive + *out = new(string) + **out = **in + } + if in.MaxIdleConns != nil { + in, out := &in.MaxIdleConns, &out.MaxIdleConns + *out = new(int) + **out = **in + } + if in.PollingInterval != nil { + in, out := &in.PollingInterval, &out.PollingInterval + *out = new(int) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPInterceptorConfigurationSpec. +func (in *HTTPInterceptorConfigurationSpec) DeepCopy() *HTTPInterceptorConfigurationSpec { + if in == nil { + return nil + } + out := new(HTTPInterceptorConfigurationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPInterceptorScalingSpec) DeepCopyInto(out *HTTPInterceptorScalingSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPInterceptorScalingSpec. +func (in *HTTPInterceptorScalingSpec) DeepCopy() *HTTPInterceptorScalingSpec { + if in == nil { + return nil + } + out := new(HTTPInterceptorScalingSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPInterceptorSpec) DeepCopyInto(out *HTTPInterceptorSpec) { + *out = *in + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = new(HTTPInterceptorConfigurationSpec) + (*in).DeepCopyInto(*out) + } + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + if in.Image != nil { + in, out := &in.Image, &out.Image + *out = new(string) + **out = **in + } + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make([]v1.LocalObjectReference, len(*in)) + copy(*out, *in) + } + if in.Autoscaling != nil { + in, out := &in.Autoscaling, &out.Autoscaling + *out = new(HTTPInterceptorScalingSpec) + **out = **in + } + in.Resources.DeepCopyInto(&out.Resources) + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPInterceptorSpec. +func (in *HTTPInterceptorSpec) DeepCopy() *HTTPInterceptorSpec { + if in == nil { + return nil + } + out := new(HTTPInterceptorSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPSalingSetTargetRef) DeepCopyInto(out *HTTPSalingSetTargetRef) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPSalingSetTargetRef. +func (in *HTTPSalingSetTargetRef) DeepCopy() *HTTPSalingSetTargetRef { + if in == nil { + return nil + } + out := new(HTTPSalingSetTargetRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPScaledObject) DeepCopyInto(out *HTTPScaledObject) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPScaledObject. +func (in *HTTPScaledObject) DeepCopy() *HTTPScaledObject { + if in == nil { + return nil + } + out := new(HTTPScaledObject) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HTTPScaledObject) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPScaledObjectCondition) DeepCopyInto(out *HTTPScaledObjectCondition) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPScaledObjectCondition. +func (in *HTTPScaledObjectCondition) DeepCopy() *HTTPScaledObjectCondition { + if in == nil { + return nil + } + out := new(HTTPScaledObjectCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPScaledObjectList) DeepCopyInto(out *HTTPScaledObjectList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]HTTPScaledObject, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPScaledObjectList. +func (in *HTTPScaledObjectList) DeepCopy() *HTTPScaledObjectList { + if in == nil { + return nil + } + out := new(HTTPScaledObjectList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HTTPScaledObjectList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPScaledObjectSpec) DeepCopyInto(out *HTTPScaledObjectSpec) { + *out = *in + if in.ScalingSet != nil { + in, out := &in.ScalingSet, &out.ScalingSet + *out = new(HTTPSalingSetTargetRef) + **out = **in + } + if in.Hosts != nil { + in, out := &in.Hosts, &out.Hosts + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.PathPrefixes != nil { + in, out := &in.PathPrefixes, &out.PathPrefixes + *out = make([]string, len(*in)) + copy(*out, *in) + } + out.ScaleTargetRef = in.ScaleTargetRef + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(ReplicaStruct) + (*in).DeepCopyInto(*out) + } + if in.TargetPendingRequests != nil { + in, out := &in.TargetPendingRequests, &out.TargetPendingRequests + *out = new(int32) + **out = **in + } + if in.CooldownPeriod != nil { + in, out := &in.CooldownPeriod, &out.CooldownPeriod + *out = new(int32) + **out = **in + } + if in.ScalingMetric != nil { + in, out := &in.ScalingMetric, &out.ScalingMetric + *out = new(ScalingMetricSpec) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPScaledObjectSpec. +func (in *HTTPScaledObjectSpec) DeepCopy() *HTTPScaledObjectSpec { + if in == nil { + return nil + } + out := new(HTTPScaledObjectSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPScaledObjectStatus) DeepCopyInto(out *HTTPScaledObjectStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(Conditions, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPScaledObjectStatus. +func (in *HTTPScaledObjectStatus) DeepCopy() *HTTPScaledObjectStatus { + if in == nil { + return nil + } + out := new(HTTPScaledObjectStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPScalerConfigurationSpec) DeepCopyInto(out *HTTPScalerConfigurationSpec) { + *out = *in + if in.Port != nil { + in, out := &in.Port, &out.Port + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPScalerConfigurationSpec. +func (in *HTTPScalerConfigurationSpec) DeepCopy() *HTTPScalerConfigurationSpec { + if in == nil { + return nil + } + out := new(HTTPScalerConfigurationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPScalerSpec) DeepCopyInto(out *HTTPScalerSpec) { + *out = *in + in.Config.DeepCopyInto(&out.Config) + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + if in.Image != nil { + in, out := &in.Image, &out.Image + *out = new(string) + **out = **in + } + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make([]v1.LocalObjectReference, len(*in)) + copy(*out, *in) + } + in.Resources.DeepCopyInto(&out.Resources) + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPScalerSpec. +func (in *HTTPScalerSpec) DeepCopy() *HTTPScalerSpec { + if in == nil { + return nil + } + out := new(HTTPScalerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPScalingSet) DeepCopyInto(out *HTTPScalingSet) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPScalingSet. +func (in *HTTPScalingSet) DeepCopy() *HTTPScalingSet { + if in == nil { + return nil + } + out := new(HTTPScalingSet) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HTTPScalingSet) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPScalingSetList) DeepCopyInto(out *HTTPScalingSetList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]HTTPScalingSet, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPScalingSetList. +func (in *HTTPScalingSetList) DeepCopy() *HTTPScalingSetList { + if in == nil { + return nil + } + out := new(HTTPScalingSetList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HTTPScalingSetList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPScalingSetSpec) DeepCopyInto(out *HTTPScalingSetSpec) { + *out = *in + in.Interceptor.DeepCopyInto(&out.Interceptor) + in.Scaler.DeepCopyInto(&out.Scaler) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPScalingSetSpec. +func (in *HTTPScalingSetSpec) DeepCopy() *HTTPScalingSetSpec { + if in == nil { + return nil + } + out := new(HTTPScalingSetSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPScalingSetStatus) DeepCopyInto(out *HTTPScalingSetStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPScalingSetStatus. +func (in *HTTPScalingSetStatus) DeepCopy() *HTTPScalingSetStatus { + if in == nil { + return nil + } + out := new(HTTPScalingSetStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RateMetricSpec) DeepCopyInto(out *RateMetricSpec) { + *out = *in + out.Window = in.Window + out.Granularity = in.Granularity + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RateMetricSpec. +func (in *RateMetricSpec) DeepCopy() *RateMetricSpec { + if in == nil { + return nil + } + out := new(RateMetricSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReplicaStruct) DeepCopyInto(out *ReplicaStruct) { + *out = *in + if in.Min != nil { + in, out := &in.Min, &out.Min + *out = new(int32) + **out = **in + } + if in.Max != nil { + in, out := &in.Max, &out.Max + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReplicaStruct. +func (in *ReplicaStruct) DeepCopy() *ReplicaStruct { + if in == nil { + return nil + } + out := new(ReplicaStruct) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ScaleTargetRef) DeepCopyInto(out *ScaleTargetRef) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScaleTargetRef. +func (in *ScaleTargetRef) DeepCopy() *ScaleTargetRef { + if in == nil { + return nil + } + out := new(ScaleTargetRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ScalingMetricSpec) DeepCopyInto(out *ScalingMetricSpec) { + *out = *in + if in.Concurrency != nil { + in, out := &in.Concurrency, &out.Concurrency + *out = new(ConcurrencyMetricSpec) + **out = **in + } + if in.Rate != nil { + in, out := &in.Rate, &out.Rate + *out = new(RateMetricSpec) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScalingMetricSpec. +func (in *ScalingMetricSpec) DeepCopy() *ScalingMetricSpec { + if in == nil { + return nil + } + out := new(ScalingMetricSpec) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/canary/deployment_controller.go b/pkg/canary/deployment_controller.go index 708f4babe..0781d62ca 100644 --- a/pkg/canary/deployment_controller.go +++ b/pkg/canary/deployment_controller.go @@ -170,6 +170,10 @@ func (c *DeploymentController) ScaleFromZero(cd *flaggerv1.Canary) error { return fmt.Errorf("deployment %s.%s get query error: %w", targetName, cd.Namespace, err) } + if cd.IsHTTPScaledObject() { + return nil + } + replicas := int32p(1) if dep.Spec.Replicas != nil && *dep.Spec.Replicas > 0 { replicas = dep.Spec.Replicas @@ -272,9 +276,12 @@ func (c *DeploymentController) createPrimaryDeployment(cd *flaggerv1.Canary, inc return fmt.Errorf("makeAnnotations failed: %w", err) } - replicas := int32(1) + replicas := int32p(int32(1)) if canaryDep.Spec.Replicas != nil && *canaryDep.Spec.Replicas > 0 { - replicas = *canaryDep.Spec.Replicas + replicas = int32p(*canaryDep.Spec.Replicas) + } + if cd.IsHTTPScaledObject() { + replicas = nil } // create primary deployment @@ -296,7 +303,7 @@ func (c *DeploymentController) createPrimaryDeployment(cd *flaggerv1.Canary, inc ProgressDeadlineSeconds: canaryDep.Spec.ProgressDeadlineSeconds, MinReadySeconds: canaryDep.Spec.MinReadySeconds, RevisionHistoryLimit: canaryDep.Spec.RevisionHistoryLimit, - Replicas: int32p(replicas), + Replicas: replicas, Strategy: canaryDep.Spec.Strategy, Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ @@ -380,6 +387,9 @@ func (c *DeploymentController) Finalize(cd *flaggerv1.Canary) error { // Scale sets the canary deployment replicas func (c *DeploymentController) scale(cd *flaggerv1.Canary, replicas int32) error { + if cd.IsHTTPScaledObject() { + return nil + } targetName := cd.Spec.TargetRef.Name dep, err := c.kubeClient.AppsV1().Deployments(cd.Namespace).Get(context.TODO(), targetName, metav1.GetOptions{}) if err != nil { diff --git a/pkg/canary/factory.go b/pkg/canary/factory.go index ec3924f4a..498719f98 100644 --- a/pkg/canary/factory.go +++ b/pkg/canary/factory.go @@ -99,11 +99,20 @@ func (factory *Factory) ScalerReconciler(kind string) ScalerReconciler { includeLabelPrefix: factory.includeLabelPrefix, } + httpsoReconciler := &HTTPScaledObjectReconciler{ + logger: factory.logger, + kubeClient: factory.kubeClient, + flaggerClient: factory.flaggerClient, + includeLabelPrefix: factory.includeLabelPrefix, + } + switch kind { case "HorizontalPodAutoscaler": return hpaReconciler case "ScaledObject": return soReconciler + case "HTTPScaledObject": + return httpsoReconciler default: return nil } diff --git a/pkg/canary/http_scaled_object_reconciler.go b/pkg/canary/http_scaled_object_reconciler.go new file mode 100644 index 000000000..533261fc0 --- /dev/null +++ b/pkg/canary/http_scaled_object_reconciler.go @@ -0,0 +1,130 @@ +package canary + +import ( + "context" + "fmt" + + "go.uber.org/zap" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/util/retry" + + flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1" + http "github.com/fluxcd/flagger/pkg/apis/http/v1alpha1" + clientset "github.com/fluxcd/flagger/pkg/client/clientset/versioned" + "github.com/google/go-cmp/cmp" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// HTTPScaledObjectReconciler is a ScalerReconciler that reconciles KEDA HTTPScaledObjects. +type HTTPScaledObjectReconciler struct { + kubeClient kubernetes.Interface + flaggerClient clientset.Interface + logger *zap.SugaredLogger + includeLabelPrefix []string +} + +func (httpsor *HTTPScaledObjectReconciler) ReconcilePrimaryScaler(cd *flaggerv1.Canary, init bool) error { + if cd.Spec.AutoscalerRef != nil { + if err := httpsor.reconcilePrimaryScaler(cd, init); err != nil { + return err + } + } + return nil +} + +func (httpsor *HTTPScaledObjectReconciler) reconcilePrimaryScaler(cd *flaggerv1.Canary, init bool) error { + primaryName := fmt.Sprintf("%s-primary", cd.Spec.TargetRef.Name) + targetSo, err := httpsor.flaggerClient.HttpV1alpha1().HTTPScaledObjects(cd.Namespace).Get(context.TODO(), cd.Spec.AutoscalerRef.Name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("Keda HTTPScaledObject %s.%s get query error: %w", + cd.Spec.AutoscalerRef.Name, cd.Namespace, err) + } + targetSoClone := targetSo.DeepCopy() + primaryServiceName := fmt.Sprintf("%s-primary", targetSoClone.Spec.ScaleTargetRef.Service) + + httpsoSpec := http.HTTPScaledObjectSpec{ + Hosts: targetSoClone.Spec.Hosts, + PathPrefixes: targetSoClone.Spec.PathPrefixes, + ScaleTargetRef: http.ScaleTargetRef{ + Name: primaryName, + APIVersion: targetSoClone.Spec.ScaleTargetRef.APIVersion, + Kind: targetSoClone.Spec.ScaleTargetRef.Kind, + Service: primaryServiceName, + Port: targetSoClone.Spec.ScaleTargetRef.Port, + }, + Replicas: targetSoClone.Spec.Replicas, + TargetPendingRequests: targetSoClone.Spec.TargetPendingRequests, + CooldownPeriod: targetSoClone.Spec.CooldownPeriod, + ScalingMetric: targetSoClone.Spec.ScalingMetric, + } + + if scalingSet := cd.Spec.AutoscalerRef.PrimaryScalingSet; scalingSet != nil { + httpsoSpec.ScalingSet = &http.HTTPSalingSetTargetRef{ + Name: scalingSet.Name, + Kind: scalingSet.Kind, + } + } + + if replicas := cd.Spec.AutoscalerRef.PrimaryScalerReplicas; replicas != nil { + if minReplicas := replicas.MinReplicas; minReplicas != nil { + httpsoSpec.Replicas.Min = minReplicas + } + if maxReplicas := replicas.MaxReplicas; maxReplicas != nil { + httpsoSpec.Replicas.Max = maxReplicas + } + } + + primarySoName := fmt.Sprintf("%s-primary", cd.Spec.AutoscalerRef.Name) + primarySo, err := httpsor.flaggerClient.HttpV1alpha1().HTTPScaledObjects(cd.Namespace).Get(context.TODO(), primarySoName, metav1.GetOptions{}) + if errors.IsNotFound(err) { + primarySo = &http.HTTPScaledObject{ + ObjectMeta: makeObjectMeta(primarySoName, targetSoClone.Labels, cd), + Spec: httpsoSpec, + } + _, err = httpsor.flaggerClient.HttpV1alpha1().HTTPScaledObjects(cd.Namespace).Create(context.TODO(), primarySo, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("creating Keda HTTPScaledObject %s.%s failed: %w", + primarySo.Name, primarySo.Namespace, err) + } + httpsor.logger.With("canary", fmt.Sprintf("%s.%s", cd.Name, cd.Namespace)).Infof( + "Keda HTTPScaledObject %s.%s created", primarySo.GetName(), cd.Namespace) + return nil + } else if err != nil { + return fmt.Errorf("Keda HTTPScaledObject %s.%s get query failed: %w", + primarySo.Name, primarySo.Namespace, err) + } + + if primarySo != nil && !init { + if diff := cmp.Diff(httpsoSpec, primarySo.Spec); diff != "" { + err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { + primarySo, err := httpsor.flaggerClient.HttpV1alpha1().HTTPScaledObjects(cd.Namespace).Get(context.TODO(), primarySoName, metav1.GetOptions{}) + if err != nil { + return err + } + primarySoClone := primarySo.DeepCopy() + primarySoClone.Spec = httpsoSpec + + filteredAnnotations := includeLabelsByPrefix(primarySo.Annotations, httpsor.includeLabelPrefix) + primarySoClone.Annotations = filteredAnnotations + filteredLabels := includeLabelsByPrefix(primarySo.ObjectMeta.Labels, httpsor.includeLabelPrefix) + primarySoClone.Labels = filteredLabels + + _, err = httpsor.flaggerClient.HttpV1alpha1().HTTPScaledObjects(cd.Namespace).Update(context.TODO(), primarySoClone, metav1.UpdateOptions{}) + return err + }) + if err != nil { + return fmt.Errorf("updating HTTPScaledObject %s.%s failed: %w", primarySoName, cd.Namespace, err) + } + } + } + return nil +} + +func (httpsor *HTTPScaledObjectReconciler) PauseTargetScaler(cd *flaggerv1.Canary) error { + return nil +} + +func (httpsor *HTTPScaledObjectReconciler) ResumeTargetScaler(cd *flaggerv1.Canary) error { + return nil +} diff --git a/pkg/client/clientset/versioned/clientset.go b/pkg/client/clientset/versioned/clientset.go index c7e159c2e..2a17cca9f 100644 --- a/pkg/client/clientset/versioned/clientset.go +++ b/pkg/client/clientset/versioned/clientset.go @@ -30,6 +30,7 @@ import ( gatewayapiv1beta1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/gatewayapi/v1beta1" gloov1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/gloo/v1" gatewayv1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/gloogateway/v1" + httpv1alpha1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/http/v1alpha1" networkingv1beta1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/istio/v1beta1" kedav1alpha1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/keda/v1alpha1" kumav1alpha1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/kuma/v1alpha1" @@ -53,6 +54,7 @@ type Interface interface { GatewayapiV1beta1() gatewayapiv1beta1.GatewayapiV1beta1Interface GlooV1() gloov1.GlooV1Interface GatewayV1() gatewayv1.GatewayV1Interface + HttpV1alpha1() httpv1alpha1.HttpV1alpha1Interface NetworkingV1beta1() networkingv1beta1.NetworkingV1beta1Interface KedaV1alpha1() kedav1alpha1.KedaV1alpha1Interface KumaV1alpha1() kumav1alpha1.KumaV1alpha1Interface @@ -74,6 +76,7 @@ type Clientset struct { gatewayapiV1beta1 *gatewayapiv1beta1.GatewayapiV1beta1Client glooV1 *gloov1.GlooV1Client gatewayV1 *gatewayv1.GatewayV1Client + httpV1alpha1 *httpv1alpha1.HttpV1alpha1Client networkingV1beta1 *networkingv1beta1.NetworkingV1beta1Client kedaV1alpha1 *kedav1alpha1.KedaV1alpha1Client kumaV1alpha1 *kumav1alpha1.KumaV1alpha1Client @@ -124,6 +127,11 @@ func (c *Clientset) GatewayV1() gatewayv1.GatewayV1Interface { return c.gatewayV1 } +// HttpV1alpha1 retrieves the HttpV1alpha1Client +func (c *Clientset) HttpV1alpha1() httpv1alpha1.HttpV1alpha1Interface { + return c.httpV1alpha1 +} + // NetworkingV1beta1 retrieves the NetworkingV1beta1Client func (c *Clientset) NetworkingV1beta1() networkingv1beta1.NetworkingV1beta1Interface { return c.networkingV1beta1 @@ -240,6 +248,10 @@ func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, if err != nil { return nil, err } + cs.httpV1alpha1, err = httpv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } cs.networkingV1beta1, err = networkingv1beta1.NewForConfigAndClient(&configShallowCopy, httpClient) if err != nil { return nil, err @@ -301,6 +313,7 @@ func New(c rest.Interface) *Clientset { cs.gatewayapiV1beta1 = gatewayapiv1beta1.New(c) cs.glooV1 = gloov1.New(c) cs.gatewayV1 = gatewayv1.New(c) + cs.httpV1alpha1 = httpv1alpha1.New(c) cs.networkingV1beta1 = networkingv1beta1.New(c) cs.kedaV1alpha1 = kedav1alpha1.New(c) cs.kumaV1alpha1 = kumav1alpha1.New(c) diff --git a/pkg/client/clientset/versioned/fake/clientset_generated.go b/pkg/client/clientset/versioned/fake/clientset_generated.go index a28c20436..fa91dd862 100644 --- a/pkg/client/clientset/versioned/fake/clientset_generated.go +++ b/pkg/client/clientset/versioned/fake/clientset_generated.go @@ -36,6 +36,8 @@ import ( fakegloov1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/gloo/v1/fake" gatewayv1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/gloogateway/v1" fakegatewayv1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/gloogateway/v1/fake" + httpv1alpha1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/http/v1alpha1" + fakehttpv1alpha1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/http/v1alpha1/fake" networkingv1beta1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/istio/v1beta1" fakenetworkingv1beta1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/istio/v1beta1/fake" kedav1alpha1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/keda/v1alpha1" @@ -153,6 +155,11 @@ func (c *Clientset) GatewayV1() gatewayv1.GatewayV1Interface { return &fakegatewayv1.FakeGatewayV1{Fake: &c.Fake} } +// HttpV1alpha1 retrieves the HttpV1alpha1Client +func (c *Clientset) HttpV1alpha1() httpv1alpha1.HttpV1alpha1Interface { + return &fakehttpv1alpha1.FakeHttpV1alpha1{Fake: &c.Fake} +} + // NetworkingV1beta1 retrieves the NetworkingV1beta1Client func (c *Clientset) NetworkingV1beta1() networkingv1beta1.NetworkingV1beta1Interface { return &fakenetworkingv1beta1.FakeNetworkingV1beta1{Fake: &c.Fake} diff --git a/pkg/client/clientset/versioned/fake/register.go b/pkg/client/clientset/versioned/fake/register.go index 51ef71e0e..adbb57a05 100644 --- a/pkg/client/clientset/versioned/fake/register.go +++ b/pkg/client/clientset/versioned/fake/register.go @@ -27,6 +27,7 @@ import ( gatewayapiv1beta1 "github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1beta1" gloov1 "github.com/fluxcd/flagger/pkg/apis/gloo/v1" gatewayv1 "github.com/fluxcd/flagger/pkg/apis/gloogateway/v1" + httpv1alpha1 "github.com/fluxcd/flagger/pkg/apis/http/v1alpha1" networkingv1beta1 "github.com/fluxcd/flagger/pkg/apis/istio/v1beta1" kedav1alpha1 "github.com/fluxcd/flagger/pkg/apis/keda/v1alpha1" kumav1alpha1 "github.com/fluxcd/flagger/pkg/apis/kuma/v1alpha1" @@ -54,6 +55,7 @@ var localSchemeBuilder = runtime.SchemeBuilder{ gatewayapiv1beta1.AddToScheme, gloov1.AddToScheme, gatewayv1.AddToScheme, + httpv1alpha1.AddToScheme, networkingv1beta1.AddToScheme, kedav1alpha1.AddToScheme, kumav1alpha1.AddToScheme, diff --git a/pkg/client/clientset/versioned/scheme/register.go b/pkg/client/clientset/versioned/scheme/register.go index 2c7688f47..745966e7e 100644 --- a/pkg/client/clientset/versioned/scheme/register.go +++ b/pkg/client/clientset/versioned/scheme/register.go @@ -27,6 +27,7 @@ import ( gatewayapiv1beta1 "github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1beta1" gloov1 "github.com/fluxcd/flagger/pkg/apis/gloo/v1" gatewayv1 "github.com/fluxcd/flagger/pkg/apis/gloogateway/v1" + httpv1alpha1 "github.com/fluxcd/flagger/pkg/apis/http/v1alpha1" networkingv1beta1 "github.com/fluxcd/flagger/pkg/apis/istio/v1beta1" kedav1alpha1 "github.com/fluxcd/flagger/pkg/apis/keda/v1alpha1" kumav1alpha1 "github.com/fluxcd/flagger/pkg/apis/kuma/v1alpha1" @@ -54,6 +55,7 @@ var localSchemeBuilder = runtime.SchemeBuilder{ gatewayapiv1beta1.AddToScheme, gloov1.AddToScheme, gatewayv1.AddToScheme, + httpv1alpha1.AddToScheme, networkingv1beta1.AddToScheme, kedav1alpha1.AddToScheme, kumav1alpha1.AddToScheme, diff --git a/pkg/client/clientset/versioned/typed/http/v1alpha1/clusterhttpscalingset.go b/pkg/client/clientset/versioned/typed/http/v1alpha1/clusterhttpscalingset.go new file mode 100644 index 000000000..66a94c512 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/http/v1alpha1/clusterhttpscalingset.go @@ -0,0 +1,69 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + + v1alpha1 "github.com/fluxcd/flagger/pkg/apis/http/v1alpha1" + scheme "github.com/fluxcd/flagger/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// ClusterHTTPScalingSetsGetter has a method to return a ClusterHTTPScalingSetInterface. +// A group's client should implement this interface. +type ClusterHTTPScalingSetsGetter interface { + ClusterHTTPScalingSets(namespace string) ClusterHTTPScalingSetInterface +} + +// ClusterHTTPScalingSetInterface has methods to work with ClusterHTTPScalingSet resources. +type ClusterHTTPScalingSetInterface interface { + Create(ctx context.Context, clusterHTTPScalingSet *v1alpha1.ClusterHTTPScalingSet, opts v1.CreateOptions) (*v1alpha1.ClusterHTTPScalingSet, error) + Update(ctx context.Context, clusterHTTPScalingSet *v1alpha1.ClusterHTTPScalingSet, opts v1.UpdateOptions) (*v1alpha1.ClusterHTTPScalingSet, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, clusterHTTPScalingSet *v1alpha1.ClusterHTTPScalingSet, opts v1.UpdateOptions) (*v1alpha1.ClusterHTTPScalingSet, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.ClusterHTTPScalingSet, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.ClusterHTTPScalingSetList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.ClusterHTTPScalingSet, err error) + ClusterHTTPScalingSetExpansion +} + +// clusterHTTPScalingSets implements ClusterHTTPScalingSetInterface +type clusterHTTPScalingSets struct { + *gentype.ClientWithList[*v1alpha1.ClusterHTTPScalingSet, *v1alpha1.ClusterHTTPScalingSetList] +} + +// newClusterHTTPScalingSets returns a ClusterHTTPScalingSets +func newClusterHTTPScalingSets(c *HttpV1alpha1Client, namespace string) *clusterHTTPScalingSets { + return &clusterHTTPScalingSets{ + gentype.NewClientWithList[*v1alpha1.ClusterHTTPScalingSet, *v1alpha1.ClusterHTTPScalingSetList]( + "clusterhttpscalingsets", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *v1alpha1.ClusterHTTPScalingSet { return &v1alpha1.ClusterHTTPScalingSet{} }, + func() *v1alpha1.ClusterHTTPScalingSetList { return &v1alpha1.ClusterHTTPScalingSetList{} }), + } +} diff --git a/pkg/client/clientset/versioned/typed/http/v1alpha1/doc.go b/pkg/client/clientset/versioned/typed/http/v1alpha1/doc.go new file mode 100644 index 000000000..9c7b8cc3b --- /dev/null +++ b/pkg/client/clientset/versioned/typed/http/v1alpha1/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha1 diff --git a/pkg/client/clientset/versioned/typed/http/v1alpha1/fake/doc.go b/pkg/client/clientset/versioned/typed/http/v1alpha1/fake/doc.go new file mode 100644 index 000000000..1ccd91197 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/http/v1alpha1/fake/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/pkg/client/clientset/versioned/typed/http/v1alpha1/fake/fake_clusterhttpscalingset.go b/pkg/client/clientset/versioned/typed/http/v1alpha1/fake/fake_clusterhttpscalingset.go new file mode 100644 index 000000000..665cbed4d --- /dev/null +++ b/pkg/client/clientset/versioned/typed/http/v1alpha1/fake/fake_clusterhttpscalingset.go @@ -0,0 +1,147 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "github.com/fluxcd/flagger/pkg/apis/http/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeClusterHTTPScalingSets implements ClusterHTTPScalingSetInterface +type FakeClusterHTTPScalingSets struct { + Fake *FakeHttpV1alpha1 + ns string +} + +var clusterhttpscalingsetsResource = v1alpha1.SchemeGroupVersion.WithResource("clusterhttpscalingsets") + +var clusterhttpscalingsetsKind = v1alpha1.SchemeGroupVersion.WithKind("ClusterHTTPScalingSet") + +// Get takes name of the clusterHTTPScalingSet, and returns the corresponding clusterHTTPScalingSet object, and an error if there is any. +func (c *FakeClusterHTTPScalingSets) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.ClusterHTTPScalingSet, err error) { + emptyResult := &v1alpha1.ClusterHTTPScalingSet{} + obj, err := c.Fake. + Invokes(testing.NewGetActionWithOptions(clusterhttpscalingsetsResource, c.ns, name, options), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.ClusterHTTPScalingSet), err +} + +// List takes label and field selectors, and returns the list of ClusterHTTPScalingSets that match those selectors. +func (c *FakeClusterHTTPScalingSets) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.ClusterHTTPScalingSetList, err error) { + emptyResult := &v1alpha1.ClusterHTTPScalingSetList{} + obj, err := c.Fake. + Invokes(testing.NewListActionWithOptions(clusterhttpscalingsetsResource, clusterhttpscalingsetsKind, c.ns, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.ClusterHTTPScalingSetList{ListMeta: obj.(*v1alpha1.ClusterHTTPScalingSetList).ListMeta} + for _, item := range obj.(*v1alpha1.ClusterHTTPScalingSetList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested clusterHTTPScalingSets. +func (c *FakeClusterHTTPScalingSets) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchActionWithOptions(clusterhttpscalingsetsResource, c.ns, opts)) + +} + +// Create takes the representation of a clusterHTTPScalingSet and creates it. Returns the server's representation of the clusterHTTPScalingSet, and an error, if there is any. +func (c *FakeClusterHTTPScalingSets) Create(ctx context.Context, clusterHTTPScalingSet *v1alpha1.ClusterHTTPScalingSet, opts v1.CreateOptions) (result *v1alpha1.ClusterHTTPScalingSet, err error) { + emptyResult := &v1alpha1.ClusterHTTPScalingSet{} + obj, err := c.Fake. + Invokes(testing.NewCreateActionWithOptions(clusterhttpscalingsetsResource, c.ns, clusterHTTPScalingSet, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.ClusterHTTPScalingSet), err +} + +// Update takes the representation of a clusterHTTPScalingSet and updates it. Returns the server's representation of the clusterHTTPScalingSet, and an error, if there is any. +func (c *FakeClusterHTTPScalingSets) Update(ctx context.Context, clusterHTTPScalingSet *v1alpha1.ClusterHTTPScalingSet, opts v1.UpdateOptions) (result *v1alpha1.ClusterHTTPScalingSet, err error) { + emptyResult := &v1alpha1.ClusterHTTPScalingSet{} + obj, err := c.Fake. + Invokes(testing.NewUpdateActionWithOptions(clusterhttpscalingsetsResource, c.ns, clusterHTTPScalingSet, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.ClusterHTTPScalingSet), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeClusterHTTPScalingSets) UpdateStatus(ctx context.Context, clusterHTTPScalingSet *v1alpha1.ClusterHTTPScalingSet, opts v1.UpdateOptions) (result *v1alpha1.ClusterHTTPScalingSet, err error) { + emptyResult := &v1alpha1.ClusterHTTPScalingSet{} + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceActionWithOptions(clusterhttpscalingsetsResource, "status", c.ns, clusterHTTPScalingSet, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.ClusterHTTPScalingSet), err +} + +// Delete takes name of the clusterHTTPScalingSet and deletes it. Returns an error if one occurs. +func (c *FakeClusterHTTPScalingSets) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(clusterhttpscalingsetsResource, c.ns, name, opts), &v1alpha1.ClusterHTTPScalingSet{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeClusterHTTPScalingSets) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionActionWithOptions(clusterhttpscalingsetsResource, c.ns, opts, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.ClusterHTTPScalingSetList{}) + return err +} + +// Patch applies the patch and returns the patched clusterHTTPScalingSet. +func (c *FakeClusterHTTPScalingSets) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.ClusterHTTPScalingSet, err error) { + emptyResult := &v1alpha1.ClusterHTTPScalingSet{} + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceActionWithOptions(clusterhttpscalingsetsResource, c.ns, name, pt, data, opts, subresources...), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.ClusterHTTPScalingSet), err +} diff --git a/pkg/client/clientset/versioned/typed/http/v1alpha1/fake/fake_http_client.go b/pkg/client/clientset/versioned/typed/http/v1alpha1/fake/fake_http_client.go new file mode 100644 index 000000000..b495666c6 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/http/v1alpha1/fake/fake_http_client.go @@ -0,0 +1,48 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/fluxcd/flagger/pkg/client/clientset/versioned/typed/http/v1alpha1" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeHttpV1alpha1 struct { + *testing.Fake +} + +func (c *FakeHttpV1alpha1) ClusterHTTPScalingSets(namespace string) v1alpha1.ClusterHTTPScalingSetInterface { + return &FakeClusterHTTPScalingSets{c, namespace} +} + +func (c *FakeHttpV1alpha1) HTTPScaledObjects(namespace string) v1alpha1.HTTPScaledObjectInterface { + return &FakeHTTPScaledObjects{c, namespace} +} + +func (c *FakeHttpV1alpha1) HTTPScalingSets(namespace string) v1alpha1.HTTPScalingSetInterface { + return &FakeHTTPScalingSets{c, namespace} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeHttpV1alpha1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/pkg/client/clientset/versioned/typed/http/v1alpha1/fake/fake_httpscaledobject.go b/pkg/client/clientset/versioned/typed/http/v1alpha1/fake/fake_httpscaledobject.go new file mode 100644 index 000000000..08a4f0e06 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/http/v1alpha1/fake/fake_httpscaledobject.go @@ -0,0 +1,147 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "github.com/fluxcd/flagger/pkg/apis/http/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeHTTPScaledObjects implements HTTPScaledObjectInterface +type FakeHTTPScaledObjects struct { + Fake *FakeHttpV1alpha1 + ns string +} + +var httpscaledobjectsResource = v1alpha1.SchemeGroupVersion.WithResource("httpscaledobjects") + +var httpscaledobjectsKind = v1alpha1.SchemeGroupVersion.WithKind("HTTPScaledObject") + +// Get takes name of the hTTPScaledObject, and returns the corresponding hTTPScaledObject object, and an error if there is any. +func (c *FakeHTTPScaledObjects) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.HTTPScaledObject, err error) { + emptyResult := &v1alpha1.HTTPScaledObject{} + obj, err := c.Fake. + Invokes(testing.NewGetActionWithOptions(httpscaledobjectsResource, c.ns, name, options), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.HTTPScaledObject), err +} + +// List takes label and field selectors, and returns the list of HTTPScaledObjects that match those selectors. +func (c *FakeHTTPScaledObjects) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.HTTPScaledObjectList, err error) { + emptyResult := &v1alpha1.HTTPScaledObjectList{} + obj, err := c.Fake. + Invokes(testing.NewListActionWithOptions(httpscaledobjectsResource, httpscaledobjectsKind, c.ns, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.HTTPScaledObjectList{ListMeta: obj.(*v1alpha1.HTTPScaledObjectList).ListMeta} + for _, item := range obj.(*v1alpha1.HTTPScaledObjectList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested hTTPScaledObjects. +func (c *FakeHTTPScaledObjects) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchActionWithOptions(httpscaledobjectsResource, c.ns, opts)) + +} + +// Create takes the representation of a hTTPScaledObject and creates it. Returns the server's representation of the hTTPScaledObject, and an error, if there is any. +func (c *FakeHTTPScaledObjects) Create(ctx context.Context, hTTPScaledObject *v1alpha1.HTTPScaledObject, opts v1.CreateOptions) (result *v1alpha1.HTTPScaledObject, err error) { + emptyResult := &v1alpha1.HTTPScaledObject{} + obj, err := c.Fake. + Invokes(testing.NewCreateActionWithOptions(httpscaledobjectsResource, c.ns, hTTPScaledObject, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.HTTPScaledObject), err +} + +// Update takes the representation of a hTTPScaledObject and updates it. Returns the server's representation of the hTTPScaledObject, and an error, if there is any. +func (c *FakeHTTPScaledObjects) Update(ctx context.Context, hTTPScaledObject *v1alpha1.HTTPScaledObject, opts v1.UpdateOptions) (result *v1alpha1.HTTPScaledObject, err error) { + emptyResult := &v1alpha1.HTTPScaledObject{} + obj, err := c.Fake. + Invokes(testing.NewUpdateActionWithOptions(httpscaledobjectsResource, c.ns, hTTPScaledObject, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.HTTPScaledObject), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeHTTPScaledObjects) UpdateStatus(ctx context.Context, hTTPScaledObject *v1alpha1.HTTPScaledObject, opts v1.UpdateOptions) (result *v1alpha1.HTTPScaledObject, err error) { + emptyResult := &v1alpha1.HTTPScaledObject{} + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceActionWithOptions(httpscaledobjectsResource, "status", c.ns, hTTPScaledObject, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.HTTPScaledObject), err +} + +// Delete takes name of the hTTPScaledObject and deletes it. Returns an error if one occurs. +func (c *FakeHTTPScaledObjects) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(httpscaledobjectsResource, c.ns, name, opts), &v1alpha1.HTTPScaledObject{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeHTTPScaledObjects) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionActionWithOptions(httpscaledobjectsResource, c.ns, opts, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.HTTPScaledObjectList{}) + return err +} + +// Patch applies the patch and returns the patched hTTPScaledObject. +func (c *FakeHTTPScaledObjects) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.HTTPScaledObject, err error) { + emptyResult := &v1alpha1.HTTPScaledObject{} + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceActionWithOptions(httpscaledobjectsResource, c.ns, name, pt, data, opts, subresources...), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.HTTPScaledObject), err +} diff --git a/pkg/client/clientset/versioned/typed/http/v1alpha1/fake/fake_httpscalingset.go b/pkg/client/clientset/versioned/typed/http/v1alpha1/fake/fake_httpscalingset.go new file mode 100644 index 000000000..4dc329ad2 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/http/v1alpha1/fake/fake_httpscalingset.go @@ -0,0 +1,147 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "github.com/fluxcd/flagger/pkg/apis/http/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeHTTPScalingSets implements HTTPScalingSetInterface +type FakeHTTPScalingSets struct { + Fake *FakeHttpV1alpha1 + ns string +} + +var httpscalingsetsResource = v1alpha1.SchemeGroupVersion.WithResource("httpscalingsets") + +var httpscalingsetsKind = v1alpha1.SchemeGroupVersion.WithKind("HTTPScalingSet") + +// Get takes name of the hTTPScalingSet, and returns the corresponding hTTPScalingSet object, and an error if there is any. +func (c *FakeHTTPScalingSets) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.HTTPScalingSet, err error) { + emptyResult := &v1alpha1.HTTPScalingSet{} + obj, err := c.Fake. + Invokes(testing.NewGetActionWithOptions(httpscalingsetsResource, c.ns, name, options), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.HTTPScalingSet), err +} + +// List takes label and field selectors, and returns the list of HTTPScalingSets that match those selectors. +func (c *FakeHTTPScalingSets) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.HTTPScalingSetList, err error) { + emptyResult := &v1alpha1.HTTPScalingSetList{} + obj, err := c.Fake. + Invokes(testing.NewListActionWithOptions(httpscalingsetsResource, httpscalingsetsKind, c.ns, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.HTTPScalingSetList{ListMeta: obj.(*v1alpha1.HTTPScalingSetList).ListMeta} + for _, item := range obj.(*v1alpha1.HTTPScalingSetList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested hTTPScalingSets. +func (c *FakeHTTPScalingSets) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchActionWithOptions(httpscalingsetsResource, c.ns, opts)) + +} + +// Create takes the representation of a hTTPScalingSet and creates it. Returns the server's representation of the hTTPScalingSet, and an error, if there is any. +func (c *FakeHTTPScalingSets) Create(ctx context.Context, hTTPScalingSet *v1alpha1.HTTPScalingSet, opts v1.CreateOptions) (result *v1alpha1.HTTPScalingSet, err error) { + emptyResult := &v1alpha1.HTTPScalingSet{} + obj, err := c.Fake. + Invokes(testing.NewCreateActionWithOptions(httpscalingsetsResource, c.ns, hTTPScalingSet, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.HTTPScalingSet), err +} + +// Update takes the representation of a hTTPScalingSet and updates it. Returns the server's representation of the hTTPScalingSet, and an error, if there is any. +func (c *FakeHTTPScalingSets) Update(ctx context.Context, hTTPScalingSet *v1alpha1.HTTPScalingSet, opts v1.UpdateOptions) (result *v1alpha1.HTTPScalingSet, err error) { + emptyResult := &v1alpha1.HTTPScalingSet{} + obj, err := c.Fake. + Invokes(testing.NewUpdateActionWithOptions(httpscalingsetsResource, c.ns, hTTPScalingSet, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.HTTPScalingSet), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeHTTPScalingSets) UpdateStatus(ctx context.Context, hTTPScalingSet *v1alpha1.HTTPScalingSet, opts v1.UpdateOptions) (result *v1alpha1.HTTPScalingSet, err error) { + emptyResult := &v1alpha1.HTTPScalingSet{} + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceActionWithOptions(httpscalingsetsResource, "status", c.ns, hTTPScalingSet, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.HTTPScalingSet), err +} + +// Delete takes name of the hTTPScalingSet and deletes it. Returns an error if one occurs. +func (c *FakeHTTPScalingSets) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(httpscalingsetsResource, c.ns, name, opts), &v1alpha1.HTTPScalingSet{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeHTTPScalingSets) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionActionWithOptions(httpscalingsetsResource, c.ns, opts, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.HTTPScalingSetList{}) + return err +} + +// Patch applies the patch and returns the patched hTTPScalingSet. +func (c *FakeHTTPScalingSets) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.HTTPScalingSet, err error) { + emptyResult := &v1alpha1.HTTPScalingSet{} + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceActionWithOptions(httpscalingsetsResource, c.ns, name, pt, data, opts, subresources...), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1alpha1.HTTPScalingSet), err +} diff --git a/pkg/client/clientset/versioned/typed/http/v1alpha1/generated_expansion.go b/pkg/client/clientset/versioned/typed/http/v1alpha1/generated_expansion.go new file mode 100644 index 000000000..2244fdafb --- /dev/null +++ b/pkg/client/clientset/versioned/typed/http/v1alpha1/generated_expansion.go @@ -0,0 +1,25 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +type ClusterHTTPScalingSetExpansion interface{} + +type HTTPScaledObjectExpansion interface{} + +type HTTPScalingSetExpansion interface{} diff --git a/pkg/client/clientset/versioned/typed/http/v1alpha1/http_client.go b/pkg/client/clientset/versioned/typed/http/v1alpha1/http_client.go new file mode 100644 index 000000000..39d27e87a --- /dev/null +++ b/pkg/client/clientset/versioned/typed/http/v1alpha1/http_client.go @@ -0,0 +1,117 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "net/http" + + v1alpha1 "github.com/fluxcd/flagger/pkg/apis/http/v1alpha1" + "github.com/fluxcd/flagger/pkg/client/clientset/versioned/scheme" + rest "k8s.io/client-go/rest" +) + +type HttpV1alpha1Interface interface { + RESTClient() rest.Interface + ClusterHTTPScalingSetsGetter + HTTPScaledObjectsGetter + HTTPScalingSetsGetter +} + +// HttpV1alpha1Client is used to interact with features provided by the http.keda.sh group. +type HttpV1alpha1Client struct { + restClient rest.Interface +} + +func (c *HttpV1alpha1Client) ClusterHTTPScalingSets(namespace string) ClusterHTTPScalingSetInterface { + return newClusterHTTPScalingSets(c, namespace) +} + +func (c *HttpV1alpha1Client) HTTPScaledObjects(namespace string) HTTPScaledObjectInterface { + return newHTTPScaledObjects(c, namespace) +} + +func (c *HttpV1alpha1Client) HTTPScalingSets(namespace string) HTTPScalingSetInterface { + return newHTTPScalingSets(c, namespace) +} + +// NewForConfig creates a new HttpV1alpha1Client for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*HttpV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + httpClient, err := rest.HTTPClientFor(&config) + if err != nil { + return nil, err + } + return NewForConfigAndClient(&config, httpClient) +} + +// NewForConfigAndClient creates a new HttpV1alpha1Client for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *rest.Config, h *http.Client) (*HttpV1alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientForConfigAndClient(&config, h) + if err != nil { + return nil, err + } + return &HttpV1alpha1Client{client}, nil +} + +// NewForConfigOrDie creates a new HttpV1alpha1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *HttpV1alpha1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new HttpV1alpha1Client for the given RESTClient. +func New(c rest.Interface) *HttpV1alpha1Client { + return &HttpV1alpha1Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1alpha1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *HttpV1alpha1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/pkg/client/clientset/versioned/typed/http/v1alpha1/httpscaledobject.go b/pkg/client/clientset/versioned/typed/http/v1alpha1/httpscaledobject.go new file mode 100644 index 000000000..6cc15dcba --- /dev/null +++ b/pkg/client/clientset/versioned/typed/http/v1alpha1/httpscaledobject.go @@ -0,0 +1,69 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + + v1alpha1 "github.com/fluxcd/flagger/pkg/apis/http/v1alpha1" + scheme "github.com/fluxcd/flagger/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// HTTPScaledObjectsGetter has a method to return a HTTPScaledObjectInterface. +// A group's client should implement this interface. +type HTTPScaledObjectsGetter interface { + HTTPScaledObjects(namespace string) HTTPScaledObjectInterface +} + +// HTTPScaledObjectInterface has methods to work with HTTPScaledObject resources. +type HTTPScaledObjectInterface interface { + Create(ctx context.Context, hTTPScaledObject *v1alpha1.HTTPScaledObject, opts v1.CreateOptions) (*v1alpha1.HTTPScaledObject, error) + Update(ctx context.Context, hTTPScaledObject *v1alpha1.HTTPScaledObject, opts v1.UpdateOptions) (*v1alpha1.HTTPScaledObject, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, hTTPScaledObject *v1alpha1.HTTPScaledObject, opts v1.UpdateOptions) (*v1alpha1.HTTPScaledObject, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.HTTPScaledObject, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.HTTPScaledObjectList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.HTTPScaledObject, err error) + HTTPScaledObjectExpansion +} + +// hTTPScaledObjects implements HTTPScaledObjectInterface +type hTTPScaledObjects struct { + *gentype.ClientWithList[*v1alpha1.HTTPScaledObject, *v1alpha1.HTTPScaledObjectList] +} + +// newHTTPScaledObjects returns a HTTPScaledObjects +func newHTTPScaledObjects(c *HttpV1alpha1Client, namespace string) *hTTPScaledObjects { + return &hTTPScaledObjects{ + gentype.NewClientWithList[*v1alpha1.HTTPScaledObject, *v1alpha1.HTTPScaledObjectList]( + "httpscaledobjects", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *v1alpha1.HTTPScaledObject { return &v1alpha1.HTTPScaledObject{} }, + func() *v1alpha1.HTTPScaledObjectList { return &v1alpha1.HTTPScaledObjectList{} }), + } +} diff --git a/pkg/client/clientset/versioned/typed/http/v1alpha1/httpscalingset.go b/pkg/client/clientset/versioned/typed/http/v1alpha1/httpscalingset.go new file mode 100644 index 000000000..9203c0ff6 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/http/v1alpha1/httpscalingset.go @@ -0,0 +1,69 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + + v1alpha1 "github.com/fluxcd/flagger/pkg/apis/http/v1alpha1" + scheme "github.com/fluxcd/flagger/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// HTTPScalingSetsGetter has a method to return a HTTPScalingSetInterface. +// A group's client should implement this interface. +type HTTPScalingSetsGetter interface { + HTTPScalingSets(namespace string) HTTPScalingSetInterface +} + +// HTTPScalingSetInterface has methods to work with HTTPScalingSet resources. +type HTTPScalingSetInterface interface { + Create(ctx context.Context, hTTPScalingSet *v1alpha1.HTTPScalingSet, opts v1.CreateOptions) (*v1alpha1.HTTPScalingSet, error) + Update(ctx context.Context, hTTPScalingSet *v1alpha1.HTTPScalingSet, opts v1.UpdateOptions) (*v1alpha1.HTTPScalingSet, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, hTTPScalingSet *v1alpha1.HTTPScalingSet, opts v1.UpdateOptions) (*v1alpha1.HTTPScalingSet, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.HTTPScalingSet, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.HTTPScalingSetList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.HTTPScalingSet, err error) + HTTPScalingSetExpansion +} + +// hTTPScalingSets implements HTTPScalingSetInterface +type hTTPScalingSets struct { + *gentype.ClientWithList[*v1alpha1.HTTPScalingSet, *v1alpha1.HTTPScalingSetList] +} + +// newHTTPScalingSets returns a HTTPScalingSets +func newHTTPScalingSets(c *HttpV1alpha1Client, namespace string) *hTTPScalingSets { + return &hTTPScalingSets{ + gentype.NewClientWithList[*v1alpha1.HTTPScalingSet, *v1alpha1.HTTPScalingSetList]( + "httpscalingsets", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *v1alpha1.HTTPScalingSet { return &v1alpha1.HTTPScalingSet{} }, + func() *v1alpha1.HTTPScalingSetList { return &v1alpha1.HTTPScalingSetList{} }), + } +} diff --git a/pkg/client/informers/externalversions/factory.go b/pkg/client/informers/externalversions/factory.go index bce8238e4..79ba77de0 100644 --- a/pkg/client/informers/externalversions/factory.go +++ b/pkg/client/informers/externalversions/factory.go @@ -30,6 +30,7 @@ import ( gatewayapi "github.com/fluxcd/flagger/pkg/client/informers/externalversions/gatewayapi" gloo "github.com/fluxcd/flagger/pkg/client/informers/externalversions/gloo" gloogateway "github.com/fluxcd/flagger/pkg/client/informers/externalversions/gloogateway" + http "github.com/fluxcd/flagger/pkg/client/informers/externalversions/http" internalinterfaces "github.com/fluxcd/flagger/pkg/client/informers/externalversions/internalinterfaces" istio "github.com/fluxcd/flagger/pkg/client/informers/externalversions/istio" keda "github.com/fluxcd/flagger/pkg/client/informers/externalversions/keda" @@ -271,6 +272,7 @@ type SharedInformerFactory interface { Gatewayapi() gatewayapi.Interface Gloo() gloo.Interface Gateway() gloogateway.Interface + Http() http.Interface Networking() istio.Interface Keda() keda.Interface Kuma() kuma.Interface @@ -303,6 +305,10 @@ func (f *sharedInformerFactory) Gateway() gloogateway.Interface { return gloogateway.New(f, f.namespace, f.tweakListOptions) } +func (f *sharedInformerFactory) Http() http.Interface { + return http.New(f, f.namespace, f.tweakListOptions) +} + func (f *sharedInformerFactory) Networking() istio.Interface { return istio.New(f, f.namespace, f.tweakListOptions) } diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index 78431793a..4d29cbeb5 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -29,8 +29,9 @@ import ( gatewayapiv1beta1 "github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1beta1" gloov1 "github.com/fluxcd/flagger/pkg/apis/gloo/v1" v1 "github.com/fluxcd/flagger/pkg/apis/gloogateway/v1" + v1alpha1 "github.com/fluxcd/flagger/pkg/apis/http/v1alpha1" istiov1beta1 "github.com/fluxcd/flagger/pkg/apis/istio/v1beta1" - v1alpha1 "github.com/fluxcd/flagger/pkg/apis/keda/v1alpha1" + kedav1alpha1 "github.com/fluxcd/flagger/pkg/apis/keda/v1alpha1" kumav1alpha1 "github.com/fluxcd/flagger/pkg/apis/kuma/v1alpha1" projectcontourv1 "github.com/fluxcd/flagger/pkg/apis/projectcontour/v1" smiv1alpha1 "github.com/fluxcd/flagger/pkg/apis/smi/v1alpha1" @@ -111,8 +112,16 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource case gloov1.SchemeGroupVersion.WithResource("upstreams"): return &genericInformer{resource: resource.GroupResource(), informer: f.Gloo().V1().Upstreams().Informer()}, nil + // Group=http.keda.sh, Version=v1alpha1 + case v1alpha1.SchemeGroupVersion.WithResource("clusterhttpscalingsets"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Http().V1alpha1().ClusterHTTPScalingSets().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("httpscaledobjects"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Http().V1alpha1().HTTPScaledObjects().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("httpscalingsets"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Http().V1alpha1().HTTPScalingSets().Informer()}, nil + // Group=keda.sh, Version=v1alpha1 - case v1alpha1.SchemeGroupVersion.WithResource("scaledobjects"): + case kedav1alpha1.SchemeGroupVersion.WithResource("scaledobjects"): return &genericInformer{resource: resource.GroupResource(), informer: f.Keda().V1alpha1().ScaledObjects().Informer()}, nil // Group=kuma.io, Version=v1alpha1 diff --git a/pkg/client/informers/externalversions/http/interface.go b/pkg/client/informers/externalversions/http/interface.go new file mode 100644 index 000000000..ea0cf4502 --- /dev/null +++ b/pkg/client/informers/externalversions/http/interface.go @@ -0,0 +1,46 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package http + +import ( + v1alpha1 "github.com/fluxcd/flagger/pkg/client/informers/externalversions/http/v1alpha1" + internalinterfaces "github.com/fluxcd/flagger/pkg/client/informers/externalversions/internalinterfaces" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V1alpha1 provides access to shared informers for resources in V1alpha1. + V1alpha1() v1alpha1.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V1alpha1 returns a new v1alpha1.Interface. +func (g *group) V1alpha1() v1alpha1.Interface { + return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/pkg/client/informers/externalversions/http/v1alpha1/clusterhttpscalingset.go b/pkg/client/informers/externalversions/http/v1alpha1/clusterhttpscalingset.go new file mode 100644 index 000000000..10545cf3e --- /dev/null +++ b/pkg/client/informers/externalversions/http/v1alpha1/clusterhttpscalingset.go @@ -0,0 +1,90 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + httpv1alpha1 "github.com/fluxcd/flagger/pkg/apis/http/v1alpha1" + versioned "github.com/fluxcd/flagger/pkg/client/clientset/versioned" + internalinterfaces "github.com/fluxcd/flagger/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/fluxcd/flagger/pkg/client/listers/http/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// ClusterHTTPScalingSetInformer provides access to a shared informer and lister for +// ClusterHTTPScalingSets. +type ClusterHTTPScalingSetInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.ClusterHTTPScalingSetLister +} + +type clusterHTTPScalingSetInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewClusterHTTPScalingSetInformer constructs a new informer for ClusterHTTPScalingSet type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewClusterHTTPScalingSetInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredClusterHTTPScalingSetInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredClusterHTTPScalingSetInformer constructs a new informer for ClusterHTTPScalingSet type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredClusterHTTPScalingSetInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HttpV1alpha1().ClusterHTTPScalingSets(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HttpV1alpha1().ClusterHTTPScalingSets(namespace).Watch(context.TODO(), options) + }, + }, + &httpv1alpha1.ClusterHTTPScalingSet{}, + resyncPeriod, + indexers, + ) +} + +func (f *clusterHTTPScalingSetInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredClusterHTTPScalingSetInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *clusterHTTPScalingSetInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&httpv1alpha1.ClusterHTTPScalingSet{}, f.defaultInformer) +} + +func (f *clusterHTTPScalingSetInformer) Lister() v1alpha1.ClusterHTTPScalingSetLister { + return v1alpha1.NewClusterHTTPScalingSetLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/http/v1alpha1/httpscaledobject.go b/pkg/client/informers/externalversions/http/v1alpha1/httpscaledobject.go new file mode 100644 index 000000000..ecc194c9f --- /dev/null +++ b/pkg/client/informers/externalversions/http/v1alpha1/httpscaledobject.go @@ -0,0 +1,90 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + httpv1alpha1 "github.com/fluxcd/flagger/pkg/apis/http/v1alpha1" + versioned "github.com/fluxcd/flagger/pkg/client/clientset/versioned" + internalinterfaces "github.com/fluxcd/flagger/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/fluxcd/flagger/pkg/client/listers/http/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// HTTPScaledObjectInformer provides access to a shared informer and lister for +// HTTPScaledObjects. +type HTTPScaledObjectInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.HTTPScaledObjectLister +} + +type hTTPScaledObjectInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewHTTPScaledObjectInformer constructs a new informer for HTTPScaledObject type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewHTTPScaledObjectInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredHTTPScaledObjectInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredHTTPScaledObjectInformer constructs a new informer for HTTPScaledObject type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredHTTPScaledObjectInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HttpV1alpha1().HTTPScaledObjects(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HttpV1alpha1().HTTPScaledObjects(namespace).Watch(context.TODO(), options) + }, + }, + &httpv1alpha1.HTTPScaledObject{}, + resyncPeriod, + indexers, + ) +} + +func (f *hTTPScaledObjectInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredHTTPScaledObjectInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *hTTPScaledObjectInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&httpv1alpha1.HTTPScaledObject{}, f.defaultInformer) +} + +func (f *hTTPScaledObjectInformer) Lister() v1alpha1.HTTPScaledObjectLister { + return v1alpha1.NewHTTPScaledObjectLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/http/v1alpha1/httpscalingset.go b/pkg/client/informers/externalversions/http/v1alpha1/httpscalingset.go new file mode 100644 index 000000000..8a7f62226 --- /dev/null +++ b/pkg/client/informers/externalversions/http/v1alpha1/httpscalingset.go @@ -0,0 +1,90 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + httpv1alpha1 "github.com/fluxcd/flagger/pkg/apis/http/v1alpha1" + versioned "github.com/fluxcd/flagger/pkg/client/clientset/versioned" + internalinterfaces "github.com/fluxcd/flagger/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "github.com/fluxcd/flagger/pkg/client/listers/http/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// HTTPScalingSetInformer provides access to a shared informer and lister for +// HTTPScalingSets. +type HTTPScalingSetInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.HTTPScalingSetLister +} + +type hTTPScalingSetInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewHTTPScalingSetInformer constructs a new informer for HTTPScalingSet type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewHTTPScalingSetInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredHTTPScalingSetInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredHTTPScalingSetInformer constructs a new informer for HTTPScalingSet type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredHTTPScalingSetInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HttpV1alpha1().HTTPScalingSets(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HttpV1alpha1().HTTPScalingSets(namespace).Watch(context.TODO(), options) + }, + }, + &httpv1alpha1.HTTPScalingSet{}, + resyncPeriod, + indexers, + ) +} + +func (f *hTTPScalingSetInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredHTTPScalingSetInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *hTTPScalingSetInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&httpv1alpha1.HTTPScalingSet{}, f.defaultInformer) +} + +func (f *hTTPScalingSetInformer) Lister() v1alpha1.HTTPScalingSetLister { + return v1alpha1.NewHTTPScalingSetLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/http/v1alpha1/interface.go b/pkg/client/informers/externalversions/http/v1alpha1/interface.go new file mode 100644 index 000000000..8423c6fdb --- /dev/null +++ b/pkg/client/informers/externalversions/http/v1alpha1/interface.go @@ -0,0 +1,59 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + internalinterfaces "github.com/fluxcd/flagger/pkg/client/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // ClusterHTTPScalingSets returns a ClusterHTTPScalingSetInformer. + ClusterHTTPScalingSets() ClusterHTTPScalingSetInformer + // HTTPScaledObjects returns a HTTPScaledObjectInformer. + HTTPScaledObjects() HTTPScaledObjectInformer + // HTTPScalingSets returns a HTTPScalingSetInformer. + HTTPScalingSets() HTTPScalingSetInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// ClusterHTTPScalingSets returns a ClusterHTTPScalingSetInformer. +func (v *version) ClusterHTTPScalingSets() ClusterHTTPScalingSetInformer { + return &clusterHTTPScalingSetInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + +// HTTPScaledObjects returns a HTTPScaledObjectInformer. +func (v *version) HTTPScaledObjects() HTTPScaledObjectInformer { + return &hTTPScaledObjectInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + +// HTTPScalingSets returns a HTTPScalingSetInformer. +func (v *version) HTTPScalingSets() HTTPScalingSetInformer { + return &hTTPScalingSetInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/client/listers/http/v1alpha1/clusterhttpscalingset.go b/pkg/client/listers/http/v1alpha1/clusterhttpscalingset.go new file mode 100644 index 000000000..1b3b1b765 --- /dev/null +++ b/pkg/client/listers/http/v1alpha1/clusterhttpscalingset.go @@ -0,0 +1,70 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/fluxcd/flagger/pkg/apis/http/v1alpha1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/listers" + "k8s.io/client-go/tools/cache" +) + +// ClusterHTTPScalingSetLister helps list ClusterHTTPScalingSets. +// All objects returned here must be treated as read-only. +type ClusterHTTPScalingSetLister interface { + // List lists all ClusterHTTPScalingSets in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.ClusterHTTPScalingSet, err error) + // ClusterHTTPScalingSets returns an object that can list and get ClusterHTTPScalingSets. + ClusterHTTPScalingSets(namespace string) ClusterHTTPScalingSetNamespaceLister + ClusterHTTPScalingSetListerExpansion +} + +// clusterHTTPScalingSetLister implements the ClusterHTTPScalingSetLister interface. +type clusterHTTPScalingSetLister struct { + listers.ResourceIndexer[*v1alpha1.ClusterHTTPScalingSet] +} + +// NewClusterHTTPScalingSetLister returns a new ClusterHTTPScalingSetLister. +func NewClusterHTTPScalingSetLister(indexer cache.Indexer) ClusterHTTPScalingSetLister { + return &clusterHTTPScalingSetLister{listers.New[*v1alpha1.ClusterHTTPScalingSet](indexer, v1alpha1.Resource("clusterhttpscalingset"))} +} + +// ClusterHTTPScalingSets returns an object that can list and get ClusterHTTPScalingSets. +func (s *clusterHTTPScalingSetLister) ClusterHTTPScalingSets(namespace string) ClusterHTTPScalingSetNamespaceLister { + return clusterHTTPScalingSetNamespaceLister{listers.NewNamespaced[*v1alpha1.ClusterHTTPScalingSet](s.ResourceIndexer, namespace)} +} + +// ClusterHTTPScalingSetNamespaceLister helps list and get ClusterHTTPScalingSets. +// All objects returned here must be treated as read-only. +type ClusterHTTPScalingSetNamespaceLister interface { + // List lists all ClusterHTTPScalingSets in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.ClusterHTTPScalingSet, err error) + // Get retrieves the ClusterHTTPScalingSet from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.ClusterHTTPScalingSet, error) + ClusterHTTPScalingSetNamespaceListerExpansion +} + +// clusterHTTPScalingSetNamespaceLister implements the ClusterHTTPScalingSetNamespaceLister +// interface. +type clusterHTTPScalingSetNamespaceLister struct { + listers.ResourceIndexer[*v1alpha1.ClusterHTTPScalingSet] +} diff --git a/pkg/client/listers/http/v1alpha1/expansion_generated.go b/pkg/client/listers/http/v1alpha1/expansion_generated.go new file mode 100644 index 000000000..ba2ad2485 --- /dev/null +++ b/pkg/client/listers/http/v1alpha1/expansion_generated.go @@ -0,0 +1,43 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +// ClusterHTTPScalingSetListerExpansion allows custom methods to be added to +// ClusterHTTPScalingSetLister. +type ClusterHTTPScalingSetListerExpansion interface{} + +// ClusterHTTPScalingSetNamespaceListerExpansion allows custom methods to be added to +// ClusterHTTPScalingSetNamespaceLister. +type ClusterHTTPScalingSetNamespaceListerExpansion interface{} + +// HTTPScaledObjectListerExpansion allows custom methods to be added to +// HTTPScaledObjectLister. +type HTTPScaledObjectListerExpansion interface{} + +// HTTPScaledObjectNamespaceListerExpansion allows custom methods to be added to +// HTTPScaledObjectNamespaceLister. +type HTTPScaledObjectNamespaceListerExpansion interface{} + +// HTTPScalingSetListerExpansion allows custom methods to be added to +// HTTPScalingSetLister. +type HTTPScalingSetListerExpansion interface{} + +// HTTPScalingSetNamespaceListerExpansion allows custom methods to be added to +// HTTPScalingSetNamespaceLister. +type HTTPScalingSetNamespaceListerExpansion interface{} diff --git a/pkg/client/listers/http/v1alpha1/httpscaledobject.go b/pkg/client/listers/http/v1alpha1/httpscaledobject.go new file mode 100644 index 000000000..956cee234 --- /dev/null +++ b/pkg/client/listers/http/v1alpha1/httpscaledobject.go @@ -0,0 +1,70 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/fluxcd/flagger/pkg/apis/http/v1alpha1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/listers" + "k8s.io/client-go/tools/cache" +) + +// HTTPScaledObjectLister helps list HTTPScaledObjects. +// All objects returned here must be treated as read-only. +type HTTPScaledObjectLister interface { + // List lists all HTTPScaledObjects in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.HTTPScaledObject, err error) + // HTTPScaledObjects returns an object that can list and get HTTPScaledObjects. + HTTPScaledObjects(namespace string) HTTPScaledObjectNamespaceLister + HTTPScaledObjectListerExpansion +} + +// hTTPScaledObjectLister implements the HTTPScaledObjectLister interface. +type hTTPScaledObjectLister struct { + listers.ResourceIndexer[*v1alpha1.HTTPScaledObject] +} + +// NewHTTPScaledObjectLister returns a new HTTPScaledObjectLister. +func NewHTTPScaledObjectLister(indexer cache.Indexer) HTTPScaledObjectLister { + return &hTTPScaledObjectLister{listers.New[*v1alpha1.HTTPScaledObject](indexer, v1alpha1.Resource("httpscaledobject"))} +} + +// HTTPScaledObjects returns an object that can list and get HTTPScaledObjects. +func (s *hTTPScaledObjectLister) HTTPScaledObjects(namespace string) HTTPScaledObjectNamespaceLister { + return hTTPScaledObjectNamespaceLister{listers.NewNamespaced[*v1alpha1.HTTPScaledObject](s.ResourceIndexer, namespace)} +} + +// HTTPScaledObjectNamespaceLister helps list and get HTTPScaledObjects. +// All objects returned here must be treated as read-only. +type HTTPScaledObjectNamespaceLister interface { + // List lists all HTTPScaledObjects in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.HTTPScaledObject, err error) + // Get retrieves the HTTPScaledObject from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.HTTPScaledObject, error) + HTTPScaledObjectNamespaceListerExpansion +} + +// hTTPScaledObjectNamespaceLister implements the HTTPScaledObjectNamespaceLister +// interface. +type hTTPScaledObjectNamespaceLister struct { + listers.ResourceIndexer[*v1alpha1.HTTPScaledObject] +} diff --git a/pkg/client/listers/http/v1alpha1/httpscalingset.go b/pkg/client/listers/http/v1alpha1/httpscalingset.go new file mode 100644 index 000000000..63c9c0903 --- /dev/null +++ b/pkg/client/listers/http/v1alpha1/httpscalingset.go @@ -0,0 +1,70 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "github.com/fluxcd/flagger/pkg/apis/http/v1alpha1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/listers" + "k8s.io/client-go/tools/cache" +) + +// HTTPScalingSetLister helps list HTTPScalingSets. +// All objects returned here must be treated as read-only. +type HTTPScalingSetLister interface { + // List lists all HTTPScalingSets in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.HTTPScalingSet, err error) + // HTTPScalingSets returns an object that can list and get HTTPScalingSets. + HTTPScalingSets(namespace string) HTTPScalingSetNamespaceLister + HTTPScalingSetListerExpansion +} + +// hTTPScalingSetLister implements the HTTPScalingSetLister interface. +type hTTPScalingSetLister struct { + listers.ResourceIndexer[*v1alpha1.HTTPScalingSet] +} + +// NewHTTPScalingSetLister returns a new HTTPScalingSetLister. +func NewHTTPScalingSetLister(indexer cache.Indexer) HTTPScalingSetLister { + return &hTTPScalingSetLister{listers.New[*v1alpha1.HTTPScalingSet](indexer, v1alpha1.Resource("httpscalingset"))} +} + +// HTTPScalingSets returns an object that can list and get HTTPScalingSets. +func (s *hTTPScalingSetLister) HTTPScalingSets(namespace string) HTTPScalingSetNamespaceLister { + return hTTPScalingSetNamespaceLister{listers.NewNamespaced[*v1alpha1.HTTPScalingSet](s.ResourceIndexer, namespace)} +} + +// HTTPScalingSetNamespaceLister helps list and get HTTPScalingSets. +// All objects returned here must be treated as read-only. +type HTTPScalingSetNamespaceLister interface { + // List lists all HTTPScalingSets in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.HTTPScalingSet, err error) + // Get retrieves the HTTPScalingSet from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.HTTPScalingSet, error) + HTTPScalingSetNamespaceListerExpansion +} + +// hTTPScalingSetNamespaceLister implements the HTTPScalingSetNamespaceLister +// interface. +type hTTPScalingSetNamespaceLister struct { + listers.ResourceIndexer[*v1alpha1.HTTPScalingSet] +} diff --git a/pkg/router/factory.go b/pkg/router/factory.go index 0a62a5ef0..ab380bf86 100644 --- a/pkg/router/factory.go +++ b/pkg/router/factory.go @@ -59,18 +59,16 @@ func NewFactory(kubeConfig *restclient.Config, kubeClient kubernetes.Interface, // KubernetesRouter returns a KubernetesRouter interface implementation func (factory *Factory) KubernetesRouter(kind string, labelSelector string, labelValue string, ports map[string]int32) KubernetesRouter { - switch kind { - case "Service": + if kind == "Service" { return &KubernetesNoopRouter{} - default: // Daemonset or Deployment - return &KubernetesDefaultRouter{ - logger: factory.logger, - flaggerClient: factory.flaggerClient, - kubeClient: factory.kubeClient, - labelSelector: labelSelector, - labelValue: labelValue, - ports: ports, - } + } + return &KubernetesDefaultRouter{ + logger: factory.logger, + flaggerClient: factory.flaggerClient, + kubeClient: factory.kubeClient, + labelSelector: labelSelector, + labelValue: labelValue, + ports: ports, } } diff --git a/pkg/router/gateway_api.go b/pkg/router/gateway_api.go index bc398afda..25cdaf4e7 100644 --- a/pkg/router/gateway_api.go +++ b/pkg/router/gateway_api.go @@ -60,16 +60,64 @@ type GatewayAPIRouter struct { setOwnerRefs bool } +func (gwr *GatewayAPIRouter) getServiceRef(canary *flaggerv1.Canary) (hostNames []v1.Hostname, apexSvcName, primarySvcName, primarySvcNamespace, canarySvcName, canarySvcNamespace string, err error) { + apexSvcName, primarySvcName, canarySvcName = canary.GetServiceNames() + canarySvcNamespace = canary.Namespace + primarySvcNamespace = canary.Namespace + err = nil + + for _, host := range canary.Spec.Service.Hosts { + hostNames = append(hostNames, v1.Hostname(host)) + } + + if canary.IsHTTPScaledObject() { + canarySvcName = "keda-http-add-on-interceptor-proxy" + canarySvcNamespace = "keda" + primarySvcNamespace = "keda" + targetHTTPSo, getErr := gwr.gatewayAPIClient.HttpV1alpha1().HTTPScaledObjects(canary.Namespace).Get(context.TODO(), canary.Spec.AutoscalerRef.Name, metav1.GetOptions{}) + if getErr != nil { + err = fmt.Errorf("HTTPScaledObject %s.%s get error: %w", canary.Spec.AutoscalerRef.Name, canary.Namespace, getErr) + return + } + for _, host := range targetHTTPSo.Spec.Hosts { + hostNames = append(hostNames, v1.Hostname(host)) + } + if canary.Spec.AutoscalerRef.CanaryInterceptorProxyService != nil { + canarySvcName = canary.Spec.AutoscalerRef.CanaryInterceptorProxyService.Name + if canary.Spec.AutoscalerRef.CanaryInterceptorProxyService.Namespace != "" { + canarySvcNamespace = canary.Spec.AutoscalerRef.CanaryInterceptorProxyService.Namespace + } + } + + if canary.Spec.AutoscalerRef.PrimaryScalingSet == nil { + err = fmt.Errorf("PrimaryScalingSet must be specified when using HTTPScaledObject as a Autoscaler.") + return + } + primarySvcName = fmt.Sprintf("%s-interceptor-proxy", canary.Spec.AutoscalerRef.PrimaryScalingSet.Name) + if canary.Spec.AutoscalerRef.PrimaryScalingSet.Namespace == "" { + if canary.Spec.AutoscalerRef.CanaryInterceptorProxyService != nil { + primarySvcNamespace = canary.Spec.AutoscalerRef.CanaryInterceptorProxyService.Namespace + } + } + if canary.Spec.AutoscalerRef.PrimaryScalingSet.Namespace != "" { + primarySvcNamespace = canary.Spec.AutoscalerRef.PrimaryScalingSet.Namespace + } + } + return +} + func (gwr *GatewayAPIRouter) Reconcile(canary *flaggerv1.Canary) error { if len(canary.Spec.Service.GatewayRefs) == 0 { return fmt.Errorf("GatewayRefs must be specified when using Gateway API as a provider.") } - apexSvcName, primarySvcName, canarySvcName := canary.GetServiceNames() + hostNames, apexSvcName, primarySvcName, primarySvcNamespace, canarySvcName, canarySvcNamespace, err := gwr.getServiceRef(canary) + if err != nil { + return fmt.Errorf("getServiceRef error: %w", err) + } hrNamespace := canary.Namespace - var hostNames []v1.Hostname for _, host := range canary.Spec.Service.Hosts { hostNames = append(hostNames, v1.Hostname(host)) } @@ -97,10 +145,10 @@ func (gwr *GatewayAPIRouter) Reconcile(canary *flaggerv1.Canary) error { Filters: gwr.makeFilters(canary), BackendRefs: []v1.HTTPBackendRef{ { - BackendRef: gwr.makeBackendRef(primarySvcName, initialPrimaryWeight, canary.Spec.Service.Port), + BackendRef: gwr.makeBackendRef(primarySvcName, primarySvcNamespace, initialPrimaryWeight, canary.Spec.Service.Port), }, { - BackendRef: gwr.makeBackendRef(canarySvcName, initialCanaryWeight, canary.Spec.Service.Port), + BackendRef: gwr.makeBackendRef(canarySvcName, canarySvcNamespace, initialCanaryWeight, canary.Spec.Service.Port), }, }, }, @@ -123,7 +171,7 @@ func (gwr *GatewayAPIRouter) Reconcile(canary *flaggerv1.Canary) error { Filters: gwr.makeFilters(canary), BackendRefs: []v1.HTTPBackendRef{ { - BackendRef: gwr.makeBackendRef(primarySvcName, initialPrimaryWeight, canary.Spec.Service.Port), + BackendRef: gwr.makeBackendRef(primarySvcName, primarySvcNamespace, initialPrimaryWeight, canary.Spec.Service.Port), }, }, }) @@ -248,7 +296,10 @@ func (gwr *GatewayAPIRouter) GetRoutes(canary *flaggerv1.Canary) ( mirrored bool, err error, ) { - apexSvcName, primarySvcName, canarySvcName := canary.GetServiceNames() + _, apexSvcName, primarySvcName, primarySvcNamespace, canarySvcName, canarySvcNamespace, err := gwr.getServiceRef(canary) + if err != nil { + return 0, 0, false, err + } hrNamespace := canary.Namespace httpRoute, err := gwr.gatewayAPIClient.GatewayapiV1().HTTPRoutes(hrNamespace).Get(context.TODO(), apexSvcName, metav1.GetOptions{}) if err != nil { @@ -271,10 +322,14 @@ func (gwr *GatewayAPIRouter) GetRoutes(canary *flaggerv1.Canary) ( // A/B testing: Avoid reading the rule with only for backendRef. if len(rule.BackendRefs) == 2 { for _, backendRef := range rule.BackendRefs { - if backendRef.Name == v1.ObjectName(primarySvcName) { + ns := httpRoute.Namespace + if backendRef.Namespace != nil { + ns = string(*backendRef.Namespace) + } + if backendRef.Name == v1.ObjectName(primarySvcName) && ns == primarySvcNamespace { primaryWeight = int(*backendRef.Weight) } - if backendRef.Name == v1.ObjectName(canarySvcName) { + if backendRef.Name == v1.ObjectName(canarySvcName) && ns == canarySvcNamespace { canaryWeight = int(*backendRef.Weight) } } @@ -306,19 +361,16 @@ func (gwr *GatewayAPIRouter) SetRoutes( canaryWeight int, mirrored bool, ) error { + hostNames, apexSvcName, primarySvcName, primarySvcNamespace, canarySvcName, canarySvcNamespace, err := gwr.getServiceRef(canary) + pWeight := int32(primaryWeight) cWeight := int32(canaryWeight) - apexSvcName, primarySvcName, canarySvcName := canary.GetServiceNames() hrNamespace := canary.Namespace httpRoute, err := gwr.gatewayAPIClient.GatewayapiV1().HTTPRoutes(hrNamespace).Get(context.TODO(), apexSvcName, metav1.GetOptions{}) if err != nil { return fmt.Errorf("HTTPRoute %s.%s get error: %w", apexSvcName, hrNamespace, err) } hrClone := httpRoute.DeepCopy() - hostNames := []v1.Hostname{} - for _, host := range canary.Spec.Service.Hosts { - hostNames = append(hostNames, v1.Hostname(host)) - } matches, err := gwr.mapRouteMatches(canary.Spec.Service.Match) if err != nil { return fmt.Errorf("Invalid request matching selectors: %w", err) @@ -341,10 +393,10 @@ func (gwr *GatewayAPIRouter) SetRoutes( Filters: gwr.makeFilters(canary), BackendRefs: []v1.HTTPBackendRef{ { - BackendRef: gwr.makeBackendRef(primarySvcName, pWeight, canary.Spec.Service.Port), + BackendRef: gwr.makeBackendRef(primarySvcName, primarySvcNamespace, pWeight, canary.Spec.Service.Port), }, { - BackendRef: gwr.makeBackendRef(canarySvcName, cWeight, canary.Spec.Service.Port), + BackendRef: gwr.makeBackendRef(canarySvcName, canarySvcNamespace, cWeight, canary.Spec.Service.Port), }, }, } @@ -400,7 +452,7 @@ func (gwr *GatewayAPIRouter) SetRoutes( Filters: gwr.makeFilters(canary), BackendRefs: []v1.HTTPBackendRef{ { - BackendRef: gwr.makeBackendRef(primarySvcName, initialPrimaryWeight, canary.Spec.Service.Port), + BackendRef: gwr.makeBackendRef(primarySvcName, primarySvcNamespace, pWeight, canary.Spec.Service.Port), }, }, Timeouts: &v1.HTTPRouteTimeouts{ @@ -432,7 +484,10 @@ func (gwr *GatewayAPIRouter) Finalize(_ *flaggerv1.Canary) error { // session affinity based Canary releases. func (gwr *GatewayAPIRouter) getSessionAffinityRouteRules(canary *flaggerv1.Canary, canaryWeight int, weightedRouteRule *v1.HTTPRouteRule) ([]v1.HTTPRouteRule, error) { - _, primarySvcName, canarySvcName := canary.GetServiceNames() + _, _, primarySvcName, primarySvcNamespace, canarySvcName, canarySvcNamespace, err := gwr.getServiceRef(canary) + if err != nil { + return nil, err + } stickyRouteRule := *weightedRouteRule // If a canary run is active, we want all responses corresponding to requests hitting the canary deployment @@ -485,10 +540,10 @@ func (gwr *GatewayAPIRouter) getSessionAffinityRouteRules(canary *flaggerv1.Cana stickyRouteRule.Matches = mergedMatches stickyRouteRule.BackendRefs = []v1.HTTPBackendRef{ { - BackendRef: gwr.makeBackendRef(primarySvcName, 0, canary.Spec.Service.Port), + BackendRef: gwr.makeBackendRef(primarySvcName, primarySvcNamespace, 0, canary.Spec.Service.Port), }, { - BackendRef: gwr.makeBackendRef(canarySvcName, 100, canary.Spec.Service.Port), + BackendRef: gwr.makeBackendRef(canarySvcName, canarySvcNamespace, 100, canary.Spec.Service.Port), }, } } else { @@ -612,13 +667,14 @@ func (gwr *GatewayAPIRouter) mapRouteMatches(requestMatches []istiov1beta1.HTTPM return matches, nil } -func (gwr *GatewayAPIRouter) makeBackendRef(svcName string, weight, port int32) v1.BackendRef { +func (gwr *GatewayAPIRouter) makeBackendRef(svcName, namespace string, weight, port int32) v1.BackendRef { return v1.BackendRef{ BackendObjectReference: v1.BackendObjectReference{ - Group: (*v1.Group)(&backendRefGroup), - Kind: (*v1.Kind)(&backendRefKind), - Name: v1.ObjectName(svcName), - Port: (*v1.PortNumber)(&port), + Group: (*v1.Group)(&backendRefGroup), + Kind: (*v1.Kind)(&backendRefKind), + Name: v1.ObjectName(svcName), + Namespace: (*v1.Namespace)(&namespace), + Port: (*v1.PortNumber)(&port), }, Weight: &weight, } diff --git a/pkg/router/kubernetes_default.go b/pkg/router/kubernetes_default.go index 840005672..c2ee2718d 100644 --- a/pkg/router/kubernetes_default.go +++ b/pkg/router/kubernetes_default.go @@ -49,14 +49,18 @@ type KubernetesDefaultRouter struct { func (c *KubernetesDefaultRouter) Initialize(canary *flaggerv1.Canary) error { _, primaryName, canaryName := canary.GetServiceNames() - // canary svc - err := c.reconcileService(canary, canaryName, c.labelValue, canary.Spec.Service.Canary) - if err != nil { - return fmt.Errorf("reconcileService failed: %w", err) + isHTTPScaledObject := canary.Spec.AutoscalerRef != nil && canary.Spec.AutoscalerRef.Kind == "HTTPScaledObject" + // For HTTPScaledObject, we do not create a Service for canary as it is not used. + if !isHTTPScaledObject { + // canary svc + err := c.reconcileService(canary, canaryName, c.labelValue, canary.Spec.Service.Canary) + if err != nil { + return fmt.Errorf("reconcileService failed: %w", err) + } } // primary svc - err = c.reconcileService(canary, primaryName, fmt.Sprintf("%s-primary", c.labelValue), canary.Spec.Service.Primary) + err := c.reconcileService(canary, primaryName, fmt.Sprintf("%s-primary", c.labelValue), canary.Spec.Service.Primary) if err != nil { return fmt.Errorf("reconcileService failed: %w", err) } @@ -68,6 +72,10 @@ func (c *KubernetesDefaultRouter) Initialize(canary *flaggerv1.Canary) error { func (c *KubernetesDefaultRouter) Reconcile(canary *flaggerv1.Canary) error { apexName, _, _ := canary.GetServiceNames() + if canary.Spec.AutoscalerRef != nil && canary.Spec.AutoscalerRef.Kind == "HTTPScaledObject" { + return nil + } + // main svc err := c.reconcileService(canary, apexName, fmt.Sprintf("%s-primary", c.labelValue), canary.Spec.Service.Apex) if err != nil { From db31a61df0b3147f1099d474a385fd4dd253035f Mon Sep 17 00:00:00 2001 From: kahirokunn Date: Fri, 8 Nov 2024 11:03:07 +0900 Subject: [PATCH 3/4] Changes will be made to make it easier to review. This commit will be dropped when the review is complete. Signed-off-by: kahirokunn --- .../tutorials/keda-httpscaledobject.md | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/gitbook/tutorials/keda-httpscaledobject.md b/docs/gitbook/tutorials/keda-httpscaledobject.md index 29642afea..ef0005827 100644 --- a/docs/gitbook/tutorials/keda-httpscaledobject.md +++ b/docs/gitbook/tutorials/keda-httpscaledobject.md @@ -2,7 +2,7 @@ This guide shows you how to use Flagger with KEDA HTTPScaledObjects which will automatically scale (including to/from zero) based on incoming HTTP traffic a Canary analysis run. -![Flagger Canary Stages](https://raw.githubusercontent.com/fluxcd/flagger/main/docs/diagrams/flagger-keda-http-add-on.png) +![Flagger Canary Stages](https://github.com/fluxcd/flagger/blob/dbde37581e052e74e3456cafaf8ec37d3a9e8c77/docs/diagrams/flagger-keda-http-add-on.png) ## Prerequisites @@ -28,13 +28,20 @@ Install KEDA: ```bash helm repo add kedacore https://kedacore.github.io/charts helm repo update -helm install keda kedacore/keda --namespace keda --create-namespace +helm upgrade -i keda kedacore/keda --namespace keda --create-namespace --version 2.15.1 ``` Install KEDA HTTP Add-on: ```bash -helm install http-add-on kedacore/keda-add-ons-http --namespace keda +helm upgrade -i http-add-on oci://quay.io/kahirokunn/keda/keda-add-ons-http \ + --version 0.9.0 \ + -n keda \ + --create-namespace \ + --set images.tag=0.9.0 \ + --set images.operator=quay.io/kahirokunn/http-add-on-operator \ + --set images.interceptor=quay.io/kahirokunn/http-add-on-interceptor \ + --set images.scaler=quay.io/kahirokunn/http-add-on-scaler ``` Install Flagger: @@ -43,12 +50,14 @@ Install Flagger: helm repo add flagger https://flagger.app helm repo update -helm upgrade -i flagger flagger/flagger \ +helm upgrade -i flagger oci://quay.io/kahirokunn/flagger/flagger \ --namespace flagger-system \ --create-namespace \ --set prometheus.install=false \ --set meshProvider=gatewayapi:v1 \ - --set metricsServer=http://prometheus.istio-system:9090 + --set metricsServer=http://prometheus.istio-system:9090 \ + --set image.repository=quay.io/kahirokunn/flagger \ + --set image.tag=latest ``` > Note: The above installation sets the mesh provider to be `gatewayapi:v1`. If your Gateway API implementation uses the `v1beta1` CRDs, then @@ -89,6 +98,7 @@ metadata: name: primary spec: interceptor: + image: quay.io/kahirokunn/http-add-on-interceptor:0.9.0 config: adminPort: 9090 connectTimeout: 500ms @@ -106,6 +116,7 @@ spec: resources: {} serviceAccountName: keda-add-ons-http-interceptor scaler: + image: quay.io/kahirokunn/http-add-on-scaler:0.9.0 serviceAccountName: keda-add-ons-http-external-scaler ``` From 87f292982bec8fcda682975768ce934701896b79 Mon Sep 17 00:00:00 2001 From: kahirokunn Date: Tue, 10 Dec 2024 09:50:03 +0900 Subject: [PATCH 4/4] feat(gateway-api): add creation of ReferenceGrant objects for cross-namespace Gateway API support Signed-off-by: kahirokunn --- charts/flagger/templates/rbac.yaml | 2 + .../v1beta1/referencegrant_types.go | 145 ++++++++++++++++++ pkg/apis/gatewayapi/v1beta1/register.go | 2 + .../v1beta1/zz_generated.deepcopy.go | 125 +++++++++++++++ .../v1beta1/fake/fake_gatewayapi_client.go | 4 + .../v1beta1/fake/fake_referencegrant.go | 134 ++++++++++++++++ .../gatewayapi/v1beta1/gatewayapi_client.go | 5 + .../gatewayapi/v1beta1/generated_expansion.go | 2 + .../gatewayapi/v1beta1/referencegrant.go | 67 ++++++++ .../gatewayapi/v1beta1/interface.go | 7 + .../gatewayapi/v1beta1/referencegrant.go | 90 +++++++++++ .../informers/externalversions/generic.go | 2 + .../gatewayapi/v1beta1/expansion_generated.go | 8 + .../gatewayapi/v1beta1/referencegrant.go | 70 +++++++++ pkg/router/gateway_api.go | 85 +++++++++- 15 files changed, 747 insertions(+), 1 deletion(-) create mode 100644 pkg/apis/gatewayapi/v1beta1/referencegrant_types.go create mode 100644 pkg/client/clientset/versioned/typed/gatewayapi/v1beta1/fake/fake_referencegrant.go create mode 100644 pkg/client/clientset/versioned/typed/gatewayapi/v1beta1/referencegrant.go create mode 100644 pkg/client/informers/externalversions/gatewayapi/v1beta1/referencegrant.go create mode 100644 pkg/client/listers/gatewayapi/v1beta1/referencegrant.go diff --git a/charts/flagger/templates/rbac.yaml b/charts/flagger/templates/rbac.yaml index 6085fd8b3..aad756e0a 100644 --- a/charts/flagger/templates/rbac.yaml +++ b/charts/flagger/templates/rbac.yaml @@ -226,6 +226,8 @@ rules: resources: - httproutes - httproutes/finalizers + - referencegrants + - referencegrants/finalizers verbs: - get - list diff --git a/pkg/apis/gatewayapi/v1beta1/referencegrant_types.go b/pkg/apis/gatewayapi/v1beta1/referencegrant_types.go new file mode 100644 index 000000000..443bc77cf --- /dev/null +++ b/pkg/apis/gatewayapi/v1beta1/referencegrant_types.go @@ -0,0 +1,145 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +// +kubebuilder:resource:categories=gateway-api,shortName=refgrant +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:storageversion + +// ReferenceGrant identifies kinds of resources in other namespaces that are +// trusted to reference the specified kinds of resources in the same namespace +// as the policy. +// +// Each ReferenceGrant can be used to represent a unique trust relationship. +// Additional Reference Grants can be used to add to the set of trusted +// sources of inbound references for the namespace they are defined within. +// +// All cross-namespace references in Gateway API (with the exception of cross-namespace +// Gateway-route attachment) require a ReferenceGrant. +// +// ReferenceGrant is a form of runtime verification allowing users to assert +// which cross-namespace object references are permitted. Implementations that +// support ReferenceGrant MUST NOT permit cross-namespace references which have +// no grant, and MUST respond to the removal of a grant by revoking the access +// that the grant allowed. +type ReferenceGrant struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec defines the desired state of ReferenceGrant. + Spec ReferenceGrantSpec `json:"spec,omitempty"` + + // Note that `Status` sub-resource has been excluded at the + // moment as it was difficult to work out the design. + // `Status` sub-resource may be added in future. +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +// ReferenceGrantList contains a list of ReferenceGrant. +type ReferenceGrantList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ReferenceGrant `json:"items"` +} + +// ReferenceGrantSpec identifies a cross namespace relationship that is trusted +// for Gateway API. +type ReferenceGrantSpec struct { + // From describes the trusted namespaces and kinds that can reference the + // resources described in "To". Each entry in this list MUST be considered + // to be an additional place that references can be valid from, or to put + // this another way, entries MUST be combined using OR. + // + // Support: Core + // + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=16 + From []ReferenceGrantFrom `json:"from"` + + // To describes the resources that may be referenced by the resources + // described in "From". Each entry in this list MUST be considered to be an + // additional place that references can be valid to, or to put this another + // way, entries MUST be combined using OR. + // + // Support: Core + // + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=16 + To []ReferenceGrantTo `json:"to"` +} + +// ReferenceGrantFrom describes trusted namespaces and kinds. +type ReferenceGrantFrom struct { + // Group is the group of the referent. + // When empty, the Kubernetes core API group is inferred. + // + // Support: Core + Group Group `json:"group"` + + // Kind is the kind of the referent. Although implementations may support + // additional resources, the following types are part of the "Core" + // support level for this field. + // + // When used to permit a SecretObjectReference: + // + // * Gateway + // + // When used to permit a BackendObjectReference: + // + // * GRPCRoute + // * HTTPRoute + // * TCPRoute + // * TLSRoute + // * UDPRoute + Kind Kind `json:"kind"` + + // Namespace is the namespace of the referent. + // + // Support: Core + Namespace Namespace `json:"namespace"` +} + +// ReferenceGrantTo describes what Kinds are allowed as targets of the +// references. +type ReferenceGrantTo struct { + // Group is the group of the referent. + // When empty, the Kubernetes core API group is inferred. + // + // Support: Core + Group Group `json:"group"` + + // Kind is the kind of the referent. Although implementations may support + // additional resources, the following types are part of the "Core" + // support level for this field: + // + // * Secret when used to permit a SecretObjectReference + // * Service when used to permit a BackendObjectReference + Kind Kind `json:"kind"` + + // Name is the name of the referent. When unspecified, this policy + // refers to all resources of the specified Group and Kind in the local + // namespace. + // + // +optional + Name *ObjectName `json:"name,omitempty"` +} diff --git a/pkg/apis/gatewayapi/v1beta1/register.go b/pkg/apis/gatewayapi/v1beta1/register.go index 930a1d8cf..231d8f792 100644 --- a/pkg/apis/gatewayapi/v1beta1/register.go +++ b/pkg/apis/gatewayapi/v1beta1/register.go @@ -33,6 +33,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &HTTPRoute{}, &HTTPRouteList{}, + &ReferenceGrant{}, + &ReferenceGrantList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/pkg/apis/gatewayapi/v1beta1/zz_generated.deepcopy.go b/pkg/apis/gatewayapi/v1beta1/zz_generated.deepcopy.go index 7938a9141..6825cb294 100644 --- a/pkg/apis/gatewayapi/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/gatewayapi/v1beta1/zz_generated.deepcopy.go @@ -643,6 +643,131 @@ func (in *ParentReference) DeepCopy() *ParentReference { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReferenceGrant) DeepCopyInto(out *ReferenceGrant) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReferenceGrant. +func (in *ReferenceGrant) DeepCopy() *ReferenceGrant { + if in == nil { + return nil + } + out := new(ReferenceGrant) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ReferenceGrant) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReferenceGrantFrom) DeepCopyInto(out *ReferenceGrantFrom) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReferenceGrantFrom. +func (in *ReferenceGrantFrom) DeepCopy() *ReferenceGrantFrom { + if in == nil { + return nil + } + out := new(ReferenceGrantFrom) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReferenceGrantList) DeepCopyInto(out *ReferenceGrantList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ReferenceGrant, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReferenceGrantList. +func (in *ReferenceGrantList) DeepCopy() *ReferenceGrantList { + if in == nil { + return nil + } + out := new(ReferenceGrantList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ReferenceGrantList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReferenceGrantSpec) DeepCopyInto(out *ReferenceGrantSpec) { + *out = *in + if in.From != nil { + in, out := &in.From, &out.From + *out = make([]ReferenceGrantFrom, len(*in)) + copy(*out, *in) + } + if in.To != nil { + in, out := &in.To, &out.To + *out = make([]ReferenceGrantTo, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReferenceGrantSpec. +func (in *ReferenceGrantSpec) DeepCopy() *ReferenceGrantSpec { + if in == nil { + return nil + } + out := new(ReferenceGrantSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReferenceGrantTo) DeepCopyInto(out *ReferenceGrantTo) { + *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(ObjectName) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReferenceGrantTo. +func (in *ReferenceGrantTo) DeepCopy() *ReferenceGrantTo { + if in == nil { + return nil + } + out := new(ReferenceGrantTo) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RouteParentStatus) DeepCopyInto(out *RouteParentStatus) { *out = *in diff --git a/pkg/client/clientset/versioned/typed/gatewayapi/v1beta1/fake/fake_gatewayapi_client.go b/pkg/client/clientset/versioned/typed/gatewayapi/v1beta1/fake/fake_gatewayapi_client.go index 18d99e74a..7447c9048 100644 --- a/pkg/client/clientset/versioned/typed/gatewayapi/v1beta1/fake/fake_gatewayapi_client.go +++ b/pkg/client/clientset/versioned/typed/gatewayapi/v1beta1/fake/fake_gatewayapi_client.go @@ -32,6 +32,10 @@ func (c *FakeGatewayapiV1beta1) HTTPRoutes(namespace string) v1beta1.HTTPRouteIn return &FakeHTTPRoutes{c, namespace} } +func (c *FakeGatewayapiV1beta1) ReferenceGrants(namespace string) v1beta1.ReferenceGrantInterface { + return &FakeReferenceGrants{c, namespace} +} + // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeGatewayapiV1beta1) RESTClient() rest.Interface { diff --git a/pkg/client/clientset/versioned/typed/gatewayapi/v1beta1/fake/fake_referencegrant.go b/pkg/client/clientset/versioned/typed/gatewayapi/v1beta1/fake/fake_referencegrant.go new file mode 100644 index 000000000..bf6ef0b86 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/gatewayapi/v1beta1/fake/fake_referencegrant.go @@ -0,0 +1,134 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1beta1 "github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeReferenceGrants implements ReferenceGrantInterface +type FakeReferenceGrants struct { + Fake *FakeGatewayapiV1beta1 + ns string +} + +var referencegrantsResource = v1beta1.SchemeGroupVersion.WithResource("referencegrants") + +var referencegrantsKind = v1beta1.SchemeGroupVersion.WithKind("ReferenceGrant") + +// Get takes name of the referenceGrant, and returns the corresponding referenceGrant object, and an error if there is any. +func (c *FakeReferenceGrants) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.ReferenceGrant, err error) { + emptyResult := &v1beta1.ReferenceGrant{} + obj, err := c.Fake. + Invokes(testing.NewGetActionWithOptions(referencegrantsResource, c.ns, name, options), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1beta1.ReferenceGrant), err +} + +// List takes label and field selectors, and returns the list of ReferenceGrants that match those selectors. +func (c *FakeReferenceGrants) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.ReferenceGrantList, err error) { + emptyResult := &v1beta1.ReferenceGrantList{} + obj, err := c.Fake. + Invokes(testing.NewListActionWithOptions(referencegrantsResource, referencegrantsKind, c.ns, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1beta1.ReferenceGrantList{ListMeta: obj.(*v1beta1.ReferenceGrantList).ListMeta} + for _, item := range obj.(*v1beta1.ReferenceGrantList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested referenceGrants. +func (c *FakeReferenceGrants) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchActionWithOptions(referencegrantsResource, c.ns, opts)) + +} + +// Create takes the representation of a referenceGrant and creates it. Returns the server's representation of the referenceGrant, and an error, if there is any. +func (c *FakeReferenceGrants) Create(ctx context.Context, referenceGrant *v1beta1.ReferenceGrant, opts v1.CreateOptions) (result *v1beta1.ReferenceGrant, err error) { + emptyResult := &v1beta1.ReferenceGrant{} + obj, err := c.Fake. + Invokes(testing.NewCreateActionWithOptions(referencegrantsResource, c.ns, referenceGrant, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1beta1.ReferenceGrant), err +} + +// Update takes the representation of a referenceGrant and updates it. Returns the server's representation of the referenceGrant, and an error, if there is any. +func (c *FakeReferenceGrants) Update(ctx context.Context, referenceGrant *v1beta1.ReferenceGrant, opts v1.UpdateOptions) (result *v1beta1.ReferenceGrant, err error) { + emptyResult := &v1beta1.ReferenceGrant{} + obj, err := c.Fake. + Invokes(testing.NewUpdateActionWithOptions(referencegrantsResource, c.ns, referenceGrant, opts), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1beta1.ReferenceGrant), err +} + +// Delete takes name of the referenceGrant and deletes it. Returns an error if one occurs. +func (c *FakeReferenceGrants) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(referencegrantsResource, c.ns, name, opts), &v1beta1.ReferenceGrant{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeReferenceGrants) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionActionWithOptions(referencegrantsResource, c.ns, opts, listOpts) + + _, err := c.Fake.Invokes(action, &v1beta1.ReferenceGrantList{}) + return err +} + +// Patch applies the patch and returns the patched referenceGrant. +func (c *FakeReferenceGrants) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.ReferenceGrant, err error) { + emptyResult := &v1beta1.ReferenceGrant{} + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceActionWithOptions(referencegrantsResource, c.ns, name, pt, data, opts, subresources...), emptyResult) + + if obj == nil { + return emptyResult, err + } + return obj.(*v1beta1.ReferenceGrant), err +} diff --git a/pkg/client/clientset/versioned/typed/gatewayapi/v1beta1/gatewayapi_client.go b/pkg/client/clientset/versioned/typed/gatewayapi/v1beta1/gatewayapi_client.go index 33f1724af..5743ad66a 100644 --- a/pkg/client/clientset/versioned/typed/gatewayapi/v1beta1/gatewayapi_client.go +++ b/pkg/client/clientset/versioned/typed/gatewayapi/v1beta1/gatewayapi_client.go @@ -29,6 +29,7 @@ import ( type GatewayapiV1beta1Interface interface { RESTClient() rest.Interface HTTPRoutesGetter + ReferenceGrantsGetter } // GatewayapiV1beta1Client is used to interact with features provided by the gatewayapi group. @@ -40,6 +41,10 @@ func (c *GatewayapiV1beta1Client) HTTPRoutes(namespace string) HTTPRouteInterfac return newHTTPRoutes(c, namespace) } +func (c *GatewayapiV1beta1Client) ReferenceGrants(namespace string) ReferenceGrantInterface { + return newReferenceGrants(c, namespace) +} + // NewForConfig creates a new GatewayapiV1beta1Client for the given config. // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), // where httpClient was generated with rest.HTTPClientFor(c). diff --git a/pkg/client/clientset/versioned/typed/gatewayapi/v1beta1/generated_expansion.go b/pkg/client/clientset/versioned/typed/gatewayapi/v1beta1/generated_expansion.go index 7cd07e728..b43e0c844 100644 --- a/pkg/client/clientset/versioned/typed/gatewayapi/v1beta1/generated_expansion.go +++ b/pkg/client/clientset/versioned/typed/gatewayapi/v1beta1/generated_expansion.go @@ -19,3 +19,5 @@ limitations under the License. package v1beta1 type HTTPRouteExpansion interface{} + +type ReferenceGrantExpansion interface{} diff --git a/pkg/client/clientset/versioned/typed/gatewayapi/v1beta1/referencegrant.go b/pkg/client/clientset/versioned/typed/gatewayapi/v1beta1/referencegrant.go new file mode 100644 index 000000000..76a62e58c --- /dev/null +++ b/pkg/client/clientset/versioned/typed/gatewayapi/v1beta1/referencegrant.go @@ -0,0 +1,67 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1beta1 + +import ( + "context" + + v1beta1 "github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1beta1" + scheme "github.com/fluxcd/flagger/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// ReferenceGrantsGetter has a method to return a ReferenceGrantInterface. +// A group's client should implement this interface. +type ReferenceGrantsGetter interface { + ReferenceGrants(namespace string) ReferenceGrantInterface +} + +// ReferenceGrantInterface has methods to work with ReferenceGrant resources. +type ReferenceGrantInterface interface { + Create(ctx context.Context, referenceGrant *v1beta1.ReferenceGrant, opts v1.CreateOptions) (*v1beta1.ReferenceGrant, error) + Update(ctx context.Context, referenceGrant *v1beta1.ReferenceGrant, opts v1.UpdateOptions) (*v1beta1.ReferenceGrant, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1beta1.ReferenceGrant, error) + List(ctx context.Context, opts v1.ListOptions) (*v1beta1.ReferenceGrantList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.ReferenceGrant, err error) + ReferenceGrantExpansion +} + +// referenceGrants implements ReferenceGrantInterface +type referenceGrants struct { + *gentype.ClientWithList[*v1beta1.ReferenceGrant, *v1beta1.ReferenceGrantList] +} + +// newReferenceGrants returns a ReferenceGrants +func newReferenceGrants(c *GatewayapiV1beta1Client, namespace string) *referenceGrants { + return &referenceGrants{ + gentype.NewClientWithList[*v1beta1.ReferenceGrant, *v1beta1.ReferenceGrantList]( + "referencegrants", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *v1beta1.ReferenceGrant { return &v1beta1.ReferenceGrant{} }, + func() *v1beta1.ReferenceGrantList { return &v1beta1.ReferenceGrantList{} }), + } +} diff --git a/pkg/client/informers/externalversions/gatewayapi/v1beta1/interface.go b/pkg/client/informers/externalversions/gatewayapi/v1beta1/interface.go index 8b782c6ea..f858a4b0a 100644 --- a/pkg/client/informers/externalversions/gatewayapi/v1beta1/interface.go +++ b/pkg/client/informers/externalversions/gatewayapi/v1beta1/interface.go @@ -26,6 +26,8 @@ import ( type Interface interface { // HTTPRoutes returns a HTTPRouteInformer. HTTPRoutes() HTTPRouteInformer + // ReferenceGrants returns a ReferenceGrantInformer. + ReferenceGrants() ReferenceGrantInformer } type version struct { @@ -43,3 +45,8 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList func (v *version) HTTPRoutes() HTTPRouteInformer { return &hTTPRouteInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } + +// ReferenceGrants returns a ReferenceGrantInformer. +func (v *version) ReferenceGrants() ReferenceGrantInformer { + return &referenceGrantInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/client/informers/externalversions/gatewayapi/v1beta1/referencegrant.go b/pkg/client/informers/externalversions/gatewayapi/v1beta1/referencegrant.go new file mode 100644 index 000000000..464e5af79 --- /dev/null +++ b/pkg/client/informers/externalversions/gatewayapi/v1beta1/referencegrant.go @@ -0,0 +1,90 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1beta1 + +import ( + "context" + time "time" + + gatewayapiv1beta1 "github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1beta1" + versioned "github.com/fluxcd/flagger/pkg/client/clientset/versioned" + internalinterfaces "github.com/fluxcd/flagger/pkg/client/informers/externalversions/internalinterfaces" + v1beta1 "github.com/fluxcd/flagger/pkg/client/listers/gatewayapi/v1beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// ReferenceGrantInformer provides access to a shared informer and lister for +// ReferenceGrants. +type ReferenceGrantInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1beta1.ReferenceGrantLister +} + +type referenceGrantInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewReferenceGrantInformer constructs a new informer for ReferenceGrant type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewReferenceGrantInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredReferenceGrantInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredReferenceGrantInformer constructs a new informer for ReferenceGrant type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredReferenceGrantInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.GatewayapiV1beta1().ReferenceGrants(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.GatewayapiV1beta1().ReferenceGrants(namespace).Watch(context.TODO(), options) + }, + }, + &gatewayapiv1beta1.ReferenceGrant{}, + resyncPeriod, + indexers, + ) +} + +func (f *referenceGrantInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredReferenceGrantInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *referenceGrantInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&gatewayapiv1beta1.ReferenceGrant{}, f.defaultInformer) +} + +func (f *referenceGrantInformer) Lister() v1beta1.ReferenceGrantLister { + return v1beta1.NewReferenceGrantLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index 4d29cbeb5..cfbab315d 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -107,6 +107,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=gatewayapi, Version=v1beta1 case gatewayapiv1beta1.SchemeGroupVersion.WithResource("httproutes"): return &genericInformer{resource: resource.GroupResource(), informer: f.Gatewayapi().V1beta1().HTTPRoutes().Informer()}, nil + case gatewayapiv1beta1.SchemeGroupVersion.WithResource("referencegrants"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Gatewayapi().V1beta1().ReferenceGrants().Informer()}, nil // Group=gloo.solo.io, Version=v1 case gloov1.SchemeGroupVersion.WithResource("upstreams"): diff --git a/pkg/client/listers/gatewayapi/v1beta1/expansion_generated.go b/pkg/client/listers/gatewayapi/v1beta1/expansion_generated.go index 2d673fb05..44376cc74 100644 --- a/pkg/client/listers/gatewayapi/v1beta1/expansion_generated.go +++ b/pkg/client/listers/gatewayapi/v1beta1/expansion_generated.go @@ -25,3 +25,11 @@ type HTTPRouteListerExpansion interface{} // HTTPRouteNamespaceListerExpansion allows custom methods to be added to // HTTPRouteNamespaceLister. type HTTPRouteNamespaceListerExpansion interface{} + +// ReferenceGrantListerExpansion allows custom methods to be added to +// ReferenceGrantLister. +type ReferenceGrantListerExpansion interface{} + +// ReferenceGrantNamespaceListerExpansion allows custom methods to be added to +// ReferenceGrantNamespaceLister. +type ReferenceGrantNamespaceListerExpansion interface{} diff --git a/pkg/client/listers/gatewayapi/v1beta1/referencegrant.go b/pkg/client/listers/gatewayapi/v1beta1/referencegrant.go new file mode 100644 index 000000000..891df708a --- /dev/null +++ b/pkg/client/listers/gatewayapi/v1beta1/referencegrant.go @@ -0,0 +1,70 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1beta1 + +import ( + v1beta1 "github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1beta1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/listers" + "k8s.io/client-go/tools/cache" +) + +// ReferenceGrantLister helps list ReferenceGrants. +// All objects returned here must be treated as read-only. +type ReferenceGrantLister interface { + // List lists all ReferenceGrants in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1beta1.ReferenceGrant, err error) + // ReferenceGrants returns an object that can list and get ReferenceGrants. + ReferenceGrants(namespace string) ReferenceGrantNamespaceLister + ReferenceGrantListerExpansion +} + +// referenceGrantLister implements the ReferenceGrantLister interface. +type referenceGrantLister struct { + listers.ResourceIndexer[*v1beta1.ReferenceGrant] +} + +// NewReferenceGrantLister returns a new ReferenceGrantLister. +func NewReferenceGrantLister(indexer cache.Indexer) ReferenceGrantLister { + return &referenceGrantLister{listers.New[*v1beta1.ReferenceGrant](indexer, v1beta1.Resource("referencegrant"))} +} + +// ReferenceGrants returns an object that can list and get ReferenceGrants. +func (s *referenceGrantLister) ReferenceGrants(namespace string) ReferenceGrantNamespaceLister { + return referenceGrantNamespaceLister{listers.NewNamespaced[*v1beta1.ReferenceGrant](s.ResourceIndexer, namespace)} +} + +// ReferenceGrantNamespaceLister helps list and get ReferenceGrants. +// All objects returned here must be treated as read-only. +type ReferenceGrantNamespaceLister interface { + // List lists all ReferenceGrants in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1beta1.ReferenceGrant, err error) + // Get retrieves the ReferenceGrant from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1beta1.ReferenceGrant, error) + ReferenceGrantNamespaceListerExpansion +} + +// referenceGrantNamespaceLister implements the ReferenceGrantNamespaceLister +// interface. +type referenceGrantNamespaceLister struct { + listers.ResourceIndexer[*v1beta1.ReferenceGrant] +} diff --git a/pkg/router/gateway_api.go b/pkg/router/gateway_api.go index 25cdaf4e7..77e9fff01 100644 --- a/pkg/router/gateway_api.go +++ b/pkg/router/gateway_api.go @@ -25,7 +25,7 @@ import ( flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1" v1 "github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1" - "github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1beta1" + v1beta1 "github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1beta1" istiov1beta1 "github.com/fluxcd/flagger/pkg/apis/istio/v1beta1" clientset "github.com/fluxcd/flagger/pkg/client/clientset/versioned" "github.com/google/go-cmp/cmp" @@ -287,6 +287,89 @@ func (gwr *GatewayAPIRouter) Reconcile(canary *flaggerv1.Canary) error { } } + if hrNamespace != primarySvcNamespace || hrNamespace != canarySvcNamespace { + var referenceGrants []*v1beta1.ReferenceGrant + + if primarySvcNamespace == canarySvcNamespace { + rg := &v1beta1.ReferenceGrant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "keda-primary", + Namespace: canarySvcNamespace, + }, + Spec: v1beta1.ReferenceGrantSpec{ + From: []v1beta1.ReferenceGrantFrom{ + { + Group: "gateway.networking.k8s.io", + Kind: "HTTPRoute", + Namespace: v1beta1.Namespace(hrNamespace), + }, + }, + To: []v1beta1.ReferenceGrantTo{ + { + Group: "", + Kind: "Service", + }, + }, + }, + } + referenceGrants = append(referenceGrants, rg) + } else { + rgPrimary := &v1beta1.ReferenceGrant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "keda-primary", + Namespace: primarySvcNamespace, + }, + Spec: v1beta1.ReferenceGrantSpec{ + From: []v1beta1.ReferenceGrantFrom{ + { + Group: "gateway.networking.k8s.io", + Kind: "HTTPRoute", + Namespace: v1beta1.Namespace(hrNamespace), + }, + }, + To: []v1beta1.ReferenceGrantTo{ + { + Group: "", + Kind: "Service", + }, + }, + }, + } + rgCanary := &v1beta1.ReferenceGrant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "keda-canary", + Namespace: canarySvcNamespace, + }, + Spec: v1beta1.ReferenceGrantSpec{ + From: []v1beta1.ReferenceGrantFrom{ + { + Group: "gateway.networking.k8s.io", + Kind: "HTTPRoute", + Namespace: v1beta1.Namespace(hrNamespace), + }, + }, + To: []v1beta1.ReferenceGrantTo{ + { + Group: "", + Kind: "Service", + }, + }, + }, + } + referenceGrants = append(referenceGrants, rgPrimary, rgCanary) + } + + for _, rg := range referenceGrants { + _, err := gwr.gatewayAPIClient.GatewayapiV1beta1().ReferenceGrants(rg.Namespace).Create(context.TODO(), rg, metav1.CreateOptions{}) + if err == nil { + gwr.logger.Infof("ReferenceGrant %s.%s has been created", rg.Name, rg.Namespace) + } else if !errors.IsAlreadyExists(err) { + gwr.logger.Errorf("ReferenceGrant %s.%s creation error: %v", rg.Name, rg.Namespace, err) + return fmt.Errorf("ReferenceGrant %s.%s creation error: %w", rg.Name, rg.Namespace, err) + } + } + } + return nil }