From 4119e44d832aee417ccfaec61e6d1406ea6033c3 Mon Sep 17 00:00:00 2001 From: wendi gunawan Date: Wed, 25 Mar 2026 11:46:39 +0100 Subject: [PATCH] setup k3s cluster --- .gitignore | 0 ansible-k3s/ansible.cfg | 3 + ansible-k3s/hosts.ini | 37 + ansible-k3s/k3s-remote-control.yaml | 61 + ansible-prepare-k3s.yml | 32 + ansible.cfg | 3 + argocd/argocd-manifest.yaml | Bin 0 -> 3881588 bytes argocd/kustomization.yaml | 72 + cosmo-router/.helmignore | 23 + cosmo-router/Chart.yaml | 24 + cosmo-router/config.yaml | 26 + cosmo-router/graph.yaml | 62 + cosmo-router/kustomization.yaml | 58 + cosmo-router/router.json | 1 + cosmo-router/subgraphs/users/schema.graphqls | 49 + .../subgraphs/users/users-schema.graphql | 29 + cosmo-router/templates/NOTES.txt | 0 cosmo-router/templates/_helpers.tpl | 62 + cosmo-router/templates/configmap.yaml | 11 + cosmo-router/templates/deployment.yaml | 54 + cosmo-router/templates/hpa.yaml | 32 + cosmo-router/templates/ingress.yaml | 39 + cosmo-router/templates/service.yaml | 15 + cosmo-router/values.yaml | 77 + db/kustomization.yaml | 26 + db/postgres-cluster.yaml | 40 + deploy-k3s.yml | 64 + hosts.ini | 38 + infra/benthos/benthos-deployment.yaml | 122 + infra/benthos/benthos-ingress.yaml | 20 + infra/benthos/kustomization.yaml | 60 + infra/benthos/resources/resources.yaml | 16 + .../streams/ingest_request_log_id_serial.yaml | 44 + .../ingest_transaction_log_id_serial.yaml | 131 + .../streams/poll_request_log_id_serial.yaml | 28 + .../streams/poll_transaction_id_serial.yaml | 28 + ...ngest_request_log_id_hash_received_at.yaml | 26 + .../reserve/poll_request_log_received_at.yaml | 14 + infra/benthos/streams/upsert_user.yaml | 13 + .../templates/ingest_template_id_serial.yaml | 76 + .../templates/poll_template_id_serial.yaml | 59 + .../ingest_template_id_hash_received_at.yaml | 28 + .../poll_template_id_hash_received_at.yaml | 136 + infra/charts/nats/.helmignore | 26 + infra/charts/nats/Chart.yaml | 16 + infra/charts/nats/README.md | 354 ++ infra/charts/nats/UPGRADING.md | 155 + infra/charts/nats/files/config-map.yaml | 10 + infra/charts/nats/files/config/cluster.yaml | 32 + infra/charts/nats/files/config/config.yaml | 114 + infra/charts/nats/files/config/gateway.yaml | 11 + infra/charts/nats/files/config/jetstream.yaml | 23 + infra/charts/nats/files/config/leafnodes.yaml | 11 + infra/charts/nats/files/config/mqtt.yaml | 10 + infra/charts/nats/files/config/protocol.yaml | 10 + infra/charts/nats/files/config/resolver.yaml | 3 + infra/charts/nats/files/config/tls.yaml | 16 + infra/charts/nats/files/config/websocket.yaml | 12 + infra/charts/nats/files/headless-service.yaml | 24 + infra/charts/nats/files/ingress.yaml | 34 + .../nats/files/nats-box/contents-secret.yaml | 17 + .../nats-box/contexts-secret/context.yaml | 51 + .../contexts-secret/contexts-secret.yaml | 13 + .../files/nats-box/deployment/container.yaml | 49 + .../files/nats-box/deployment/deployment.yaml | 16 + .../nats-box/deployment/pod-template.yaml | 44 + .../nats/files/nats-box/service-account.yaml | 7 + .../nats/files/pod-disruption-budget.yaml | 12 + infra/charts/nats/files/pod-monitor.yaml | 13 + infra/charts/nats/files/service-account.yaml | 7 + infra/charts/nats/files/service.yaml | 23 + .../files/stateful-set/jetstream-pvc.yaml | 13 + .../files/stateful-set/nats-container.yaml | 109 + .../nats/files/stateful-set/pod-template.yaml | 75 + .../stateful-set/prom-exporter-container.yaml | 31 + .../stateful-set/reloader-container.yaml | 27 + .../nats/files/stateful-set/resolver-pvc.yaml | 13 + .../nats/files/stateful-set/stateful-set.yaml | 37 + infra/charts/nats/templates/_helpers.tpl | 289 + infra/charts/nats/templates/_jsonpatch.tpl | 219 + .../nats/templates/_toPrettyRawJson.tpl | 28 + infra/charts/nats/templates/_tplYaml.tpl | 114 + infra/charts/nats/templates/config-map.yaml | 4 + .../nats/templates/extra-resources.yaml | 5 + .../nats/templates/headless-service.yaml | 4 + infra/charts/nats/templates/ingress.yaml | 6 + .../templates/nats-box/contents-secret.yaml | 10 + .../templates/nats-box/contexts-secret.yaml | 8 + .../nats/templates/nats-box/deployment.yaml | 8 + .../templates/nats-box/service-account.yaml | 8 + .../nats/templates/pod-disruption-budget.yaml | 6 + infra/charts/nats/templates/pod-monitor.yaml | 8 + .../nats/templates/service-account.yaml | 6 + infra/charts/nats/templates/service.yaml | 6 + infra/charts/nats/templates/stateful-set.yaml | 4 + .../nats/templates/tests/request-reply.yaml | 37 + infra/charts/nats/values.yaml | 696 +++ infra/kustomization.yaml | 87 + infra/nats-cluster.yaml | Bin 0 -> 20128 bytes infra/nats-values.yaml | 22 + kustomization.yaml | 33 + linkerd/ca.crt | 11 + linkerd/ca.key | 5 + .../charts/linkerd-control-plane/.helmignore | 22 + .../charts/linkerd-control-plane/Chart.lock | 6 + .../charts/linkerd-control-plane/Chart.yaml | 22 + .../charts/linkerd-control-plane/README.md | 297 + .../linkerd-control-plane/README.md.gotmpl | 138 + .../charts/partials/.helmignore | 21 + .../charts/partials/Chart.yaml | 5 + .../charts/partials/README.md | 9 + .../charts/partials/README.md.gotmpl | 14 + .../charts/partials/templates/NOTES.txt | 0 .../charts/partials/templates/_affinity.tpl | 38 + .../partials/templates/_capabilities.tpl | 16 + .../charts/partials/templates/_debug.tpl | 6 + .../charts/partials/templates/_helpers.tpl | 14 + .../charts/partials/templates/_metadata.tpl | 17 + .../partials/templates/_network-validator.tpl | 32 + .../partials/templates/_nodeselector.tpl | 4 + .../partials/templates/_proxy-config-ann.tpl | 18 + .../charts/partials/templates/_proxy-init.tpl | 89 + .../charts/partials/templates/_proxy.tpl | 215 + .../partials/templates/_pull-secrets.tpl | 6 + .../charts/partials/templates/_resources.tpl | 28 + .../partials/templates/_tolerations.tpl | 4 + .../charts/partials/templates/_trace.tpl | 5 + .../charts/partials/templates/_validate.tpl | 19 + .../charts/partials/templates/_volumes.tpl | 20 + .../charts/partials/values.yaml | 0 .../linkerd-control-plane/templates/NOTES.txt | 19 + .../templates/config-rbac.yaml | 16 + .../templates/config.yaml | 39 + .../templates/destination-rbac.yaml | 305 + .../templates/destination.yaml | 364 ++ .../templates/heartbeat-rbac.yaml | 78 + .../templates/heartbeat.yaml | 85 + .../templates/identity-rbac.yaml | 49 + .../templates/identity.yaml | 254 + .../templates/namespace.yaml | 18 + .../templates/podmonitor.yaml | 128 + .../templates/proxy-injector-rbac.yaml | 118 + .../templates/proxy-injector.yaml | 193 + .../linkerd-control-plane/templates/psp.yaml | 110 + .../linkerd-control-plane/values-ha.yaml | 60 + .../charts/linkerd-control-plane/values.yaml | 574 ++ .../linkerd-crds/.helmignore | 22 + .../linkerd-crds/Chart.lock | 6 + .../linkerd-crds/Chart.yaml | 21 + .../linkerd-crds-1.8.0/linkerd-crds/README.md | 76 + .../linkerd-crds/README.md.gotmpl | 64 + .../linkerd-crds/charts/partials/.helmignore | 21 + .../linkerd-crds/charts/partials/Chart.yaml | 5 + .../linkerd-crds/charts/partials/README.md | 9 + .../charts/partials/README.md.gotmpl | 14 + .../charts/partials/templates/NOTES.txt | 0 .../charts/partials/templates/_affinity.tpl | 38 + .../partials/templates/_capabilities.tpl | 16 + .../charts/partials/templates/_debug.tpl | 6 + .../charts/partials/templates/_helpers.tpl | 14 + .../charts/partials/templates/_metadata.tpl | 17 + .../partials/templates/_network-validator.tpl | 32 + .../partials/templates/_nodeselector.tpl | 4 + .../partials/templates/_proxy-config-ann.tpl | 18 + .../charts/partials/templates/_proxy-init.tpl | 89 + .../charts/partials/templates/_proxy.tpl | 215 + .../partials/templates/_pull-secrets.tpl | 6 + .../charts/partials/templates/_resources.tpl | 28 + .../partials/templates/_tolerations.tpl | 4 + .../charts/partials/templates/_trace.tpl | 5 + .../charts/partials/templates/_validate.tpl | 19 + .../charts/partials/templates/_volumes.tpl | 20 + .../linkerd-crds/charts/partials/values.yaml | 0 .../linkerd-crds/templates/NOTES.txt | 6 + .../gateway.networking.k8s.io_httproutes.yaml | 4012 +++++++++++++ .../policy/authorization-policy.yaml | 99 + .../templates/policy/httproute.yaml | 5216 +++++++++++++++++ .../policy/meshtls-authentication.yaml | 88 + .../policy/network-authentication.yaml | 53 + .../policy/server-authorization.yaml | 266 + .../linkerd-crds/templates/policy/server.yaml | 132 + .../templates/serviceprofile.yaml | 278 + .../linkerd-crds/values.yaml | 1 + linkerd/charts/linkerd-crds/.helmignore | 22 + linkerd/charts/linkerd-crds/Chart.lock | 6 + linkerd/charts/linkerd-crds/Chart.yaml | 21 + linkerd/charts/linkerd-crds/README.md | 76 + linkerd/charts/linkerd-crds/README.md.gotmpl | 64 + .../linkerd-crds/charts/partials/.helmignore | 21 + .../linkerd-crds/charts/partials/Chart.yaml | 5 + .../linkerd-crds/charts/partials/README.md | 9 + .../charts/partials/README.md.gotmpl | 14 + .../charts/partials/templates/NOTES.txt | 0 .../charts/partials/templates/_affinity.tpl | 38 + .../partials/templates/_capabilities.tpl | 16 + .../charts/partials/templates/_debug.tpl | 6 + .../charts/partials/templates/_helpers.tpl | 14 + .../charts/partials/templates/_metadata.tpl | 17 + .../partials/templates/_network-validator.tpl | 32 + .../partials/templates/_nodeselector.tpl | 4 + .../partials/templates/_proxy-config-ann.tpl | 18 + .../charts/partials/templates/_proxy-init.tpl | 89 + .../charts/partials/templates/_proxy.tpl | 215 + .../partials/templates/_pull-secrets.tpl | 6 + .../charts/partials/templates/_resources.tpl | 28 + .../partials/templates/_tolerations.tpl | 4 + .../charts/partials/templates/_trace.tpl | 5 + .../charts/partials/templates/_validate.tpl | 19 + .../charts/partials/templates/_volumes.tpl | 20 + .../linkerd-crds/charts/partials/values.yaml | 0 .../charts/linkerd-crds/templates/NOTES.txt | 6 + .../gateway.networking.k8s.io_httproutes.yaml | 4012 +++++++++++++ .../policy/authorization-policy.yaml | 99 + .../templates/policy/httproute.yaml | 5216 +++++++++++++++++ .../policy/meshtls-authentication.yaml | 88 + .../policy/network-authentication.yaml | 53 + .../policy/server-authorization.yaml | 266 + .../linkerd-crds/templates/policy/server.yaml | 132 + .../templates/serviceprofile.yaml | 278 + linkerd/charts/linkerd-crds/values.yaml | 1 + linkerd/charts/linkerd-viz/.helmignore | 22 + linkerd/charts/linkerd-viz/Chart.yaml | 17 + linkerd/charts/linkerd-viz/README.md | 214 + linkerd/charts/linkerd-viz/README.md.gotmpl | 63 + .../linkerd-viz/charts/partials/.helmignore | 21 + .../linkerd-viz/charts/partials/Chart.yaml | 5 + .../linkerd-viz/charts/partials/README.md | 9 + .../charts/partials/README.md.gotmpl | 14 + .../charts/partials/templates/NOTES.txt | 0 .../charts/partials/templates/_affinity.tpl | 38 + .../partials/templates/_capabilities.tpl | 16 + .../charts/partials/templates/_debug.tpl | 6 + .../charts/partials/templates/_helpers.tpl | 14 + .../charts/partials/templates/_metadata.tpl | 17 + .../partials/templates/_network-validator.tpl | 32 + .../partials/templates/_nodeselector.tpl | 4 + .../partials/templates/_proxy-config-ann.tpl | 18 + .../charts/partials/templates/_proxy-init.tpl | 89 + .../charts/partials/templates/_proxy.tpl | 215 + .../partials/templates/_pull-secrets.tpl | 6 + .../charts/partials/templates/_resources.tpl | 28 + .../partials/templates/_tolerations.tpl | 4 + .../charts/partials/templates/_trace.tpl | 5 + .../charts/partials/templates/_validate.tpl | 19 + .../charts/partials/templates/_volumes.tpl | 20 + .../linkerd-viz/charts/partials/values.yaml | 0 linkerd/charts/linkerd-viz/requirements.lock | 6 + linkerd/charts/linkerd-viz/requirements.yaml | 4 + .../charts/linkerd-viz/templates/NOTES.txt | 11 + .../linkerd-viz/templates/admin-policy.yaml | 17 + .../templates/metrics-api-policy.yaml | 56 + .../templates/metrics-api-rbac.yaml | 56 + .../linkerd-viz/templates/metrics-api.yaml | 125 + .../templates/namespace-metadata-rbac.yaml | 77 + .../templates/namespace-metadata.yaml | 67 + .../linkerd-viz/templates/namespace.yaml | 18 + .../templates/prometheus-policy.yaml | 39 + .../templates/prometheus-rbac.yaml | 47 + .../linkerd-viz/templates/prometheus.yaml | 337 ++ linkerd/charts/linkerd-viz/templates/psp.yaml | 52 + .../templates/service-profiles.yaml | 64 + .../templates/tap-injector-policy.yaml | 57 + .../templates/tap-injector-rbac.yaml | 105 + .../linkerd-viz/templates/tap-injector.yaml | 129 + .../linkerd-viz/templates/tap-policy.yaml | 40 + .../linkerd-viz/templates/tap-rbac.yaml | 154 + linkerd/charts/linkerd-viz/templates/tap.yaml | 144 + .../linkerd-viz/templates/web-rbac.yaml | 155 + linkerd/charts/linkerd-viz/templates/web.yaml | 141 + linkerd/charts/linkerd-viz/values-ha.yaml | 35 + linkerd/charts/linkerd-viz/values.yaml | 554 ++ linkerd/control-plane.yaml | 80 + linkerd/issuer.crt | 12 + linkerd/issuer.csr | 7 + linkerd/issuer.ext | 2 + linkerd/issuer.key | 5 + linkerd/kustomization.yaml | 97 + linkerd/linkerd-control-plane.yaml | Bin 0 -> 155136 bytes linkerd/linkerd-crds.yaml | Bin 0 -> 1326920 bytes linkerd/linkerd-viz.yaml | Bin 0 -> 106058 bytes linkerd/values-identity.yaml | 55 + .../longhorn-v1.5.3/longhorn/.helmignore | 21 + .../longhorn-v1.5.3/longhorn/Chart.yaml | 28 + .../charts/longhorn-v1.5.3/longhorn/README.md | 78 + .../longhorn-v1.5.3/longhorn/app-readme.md | 11 + .../longhorn-v1.5.3/longhorn/questions.yaml | 890 +++ .../longhorn/templates/NOTES.txt | 5 + .../longhorn/templates/_helpers.tpl | 66 + .../longhorn/templates/clusterrole.yaml | 61 + .../templates/clusterrolebinding.yaml | 27 + .../longhorn/templates/crds.yaml | 3672 ++++++++++++ .../longhorn/templates/daemonset-sa.yaml | 151 + .../longhorn/templates/default-setting.yaml | 83 + .../longhorn/templates/deployment-driver.yaml | 118 + .../longhorn/templates/deployment-ui.yaml | 114 + .../longhorn/templates/ingress.yaml | 48 + ...king-image-data-source-network-policy.yaml | 27 + .../backing-image-manager-network-policy.yaml | 27 + .../instance-manager-networking.yaml | 27 + .../manager-network-policy.yaml | 35 + .../recovery-backend-network-policy.yaml | 17 + .../ui-frontend-network-policy.yaml | 46 + .../webhook-network-policy.yaml | 33 + .../longhorn/templates/postupgrade-job.yaml | 56 + .../longhorn/templates/preupgrade-job.yaml | 58 + .../longhorn/templates/psp.yaml | 66 + .../longhorn/templates/registry-secret.yaml | 13 + .../longhorn/templates/serviceaccount.yaml | 21 + .../longhorn/templates/services.yaml | 74 + .../longhorn/templates/storageclass.yaml | 44 + .../longhorn/templates/tls-secrets.yaml | 16 + .../longhorn/templates/uninstall-job.yaml | 57 + .../templates/validate-psp-install.yaml | 7 + .../longhorn-v1.5.3/longhorn/values.yaml | 296 + longhorn-system/kustomization.yaml | 24 + longhorn-system/longhorn-ingress.yaml | 19 + longhorn-system/values.yaml | 17 + manuals.md | 271 + manuals/deploy_k8s_components.md | 732 +++ manuals/important_commands.md | 139 + manuals/interconnection.md | 94 + manuals/k8s_yaml.md | 171 + manuals/setup_k8s.md | 668 +++ namespaces.yaml | 44 + namespaces/apps-ns.yaml | 4 + namespaces/argocd-ns.yaml | 4 + namespaces/db-ns.yaml | 4 + namespaces/infra-ns.yaml | 4 + namespaces/kustomization.yaml | 10 + namespaces/stream-ns.yaml | 4 + setup-local-tools.yml | 45 + .../yugabyte-2025.2.1/yugabyte/.helmignore | 1 + .../yugabyte-2025.2.1/yugabyte/Chart.yaml | 18 + .../yugabyte-2025.2.1/yugabyte/README.md | 1 + .../yugabyte-2025.2.1/yugabyte/app-readme.md | 1 + .../yugabyte/expose-all-shared.yaml | 21 + .../yugabyte/expose-all.yaml | 31 + .../yugabyte/generate_kubeconfig.py | 220 + .../yugabyte/openshift.values.yaml | 4 + .../yugabyte-2025.2.1/yugabyte/questions.yaml | 174 + .../yugabyte/templates/NOTES.txt | 29 + .../yugabyte/templates/_helpers.tpl | 674 +++ .../yugabyte/templates/certificates.yaml | 157 + .../templates/controller-gflags-secret.yaml | 36 + .../yugabyte/templates/debug_config_map.yaml | 23 + .../hooks/setup-credentials-job.yaml | 104 + .../templates/master-gflags-secret.yaml | 58 + .../templates/master-servicemonitor.yaml | 51 + .../multicluster/common-tserver-service.yaml | 23 + .../multicluster/mcs-service-export.yaml | 21 + .../multicluster/service-per-pod.yaml | 35 + .../yugabyte/templates/otel-collector.yaml | 154 + .../yugabyte/templates/secrets.yaml | 7 + .../yugabyte/templates/service-endpoints.yaml | 93 + .../yugabyte/templates/service.yaml | 874 +++ .../setup-credentials-configmap.yaml | 262 + .../templates/tserver-gflags-secret.yaml | 73 + .../templates/tserver-servicemonitor.yaml | 115 + .../yugabyte-2025.2.1/yugabyte/values.yaml | 886 +++ .../yugabyte/yugabyte-rbac.yaml | 19 + yugabyte/kustomization.yaml | 36 + yugabyte/yugabytedb.yaml | Bin 0 -> 52350 bytes yugabyte/yugabytedb2.yaml | Bin 0 -> 51418 bytes 363 files changed, 46488 insertions(+) create mode 100644 .gitignore create mode 100644 ansible-k3s/ansible.cfg create mode 100644 ansible-k3s/hosts.ini create mode 100644 ansible-k3s/k3s-remote-control.yaml create mode 100644 ansible-prepare-k3s.yml create mode 100644 ansible.cfg create mode 100644 argocd/argocd-manifest.yaml create mode 100644 argocd/kustomization.yaml create mode 100644 cosmo-router/.helmignore create mode 100644 cosmo-router/Chart.yaml create mode 100644 cosmo-router/config.yaml create mode 100644 cosmo-router/graph.yaml create mode 100644 cosmo-router/kustomization.yaml create mode 100644 cosmo-router/router.json create mode 100644 cosmo-router/subgraphs/users/schema.graphqls create mode 100644 cosmo-router/subgraphs/users/users-schema.graphql create mode 100644 cosmo-router/templates/NOTES.txt create mode 100644 cosmo-router/templates/_helpers.tpl create mode 100644 cosmo-router/templates/configmap.yaml create mode 100644 cosmo-router/templates/deployment.yaml create mode 100644 cosmo-router/templates/hpa.yaml create mode 100644 cosmo-router/templates/ingress.yaml create mode 100644 cosmo-router/templates/service.yaml create mode 100644 cosmo-router/values.yaml create mode 100644 db/kustomization.yaml create mode 100644 db/postgres-cluster.yaml create mode 100644 deploy-k3s.yml create mode 100644 hosts.ini create mode 100644 infra/benthos/benthos-deployment.yaml create mode 100644 infra/benthos/benthos-ingress.yaml create mode 100644 infra/benthos/kustomization.yaml create mode 100644 infra/benthos/resources/resources.yaml create mode 100644 infra/benthos/streams/ingest_request_log_id_serial.yaml create mode 100644 infra/benthos/streams/ingest_transaction_log_id_serial.yaml create mode 100644 infra/benthos/streams/poll_request_log_id_serial.yaml create mode 100644 infra/benthos/streams/poll_transaction_id_serial.yaml create mode 100644 infra/benthos/streams/reserve/ingest_request_log_id_hash_received_at.yaml create mode 100644 infra/benthos/streams/reserve/poll_request_log_received_at.yaml create mode 100644 infra/benthos/streams/upsert_user.yaml create mode 100644 infra/benthos/templates/ingest_template_id_serial.yaml create mode 100644 infra/benthos/templates/poll_template_id_serial.yaml create mode 100644 infra/benthos/templates/reserve/ingest_template_id_hash_received_at.yaml create mode 100644 infra/benthos/templates/reserve/poll_template_id_hash_received_at.yaml create mode 100644 infra/charts/nats/.helmignore create mode 100644 infra/charts/nats/Chart.yaml create mode 100644 infra/charts/nats/README.md create mode 100644 infra/charts/nats/UPGRADING.md create mode 100644 infra/charts/nats/files/config-map.yaml create mode 100644 infra/charts/nats/files/config/cluster.yaml create mode 100644 infra/charts/nats/files/config/config.yaml create mode 100644 infra/charts/nats/files/config/gateway.yaml create mode 100644 infra/charts/nats/files/config/jetstream.yaml create mode 100644 infra/charts/nats/files/config/leafnodes.yaml create mode 100644 infra/charts/nats/files/config/mqtt.yaml create mode 100644 infra/charts/nats/files/config/protocol.yaml create mode 100644 infra/charts/nats/files/config/resolver.yaml create mode 100644 infra/charts/nats/files/config/tls.yaml create mode 100644 infra/charts/nats/files/config/websocket.yaml create mode 100644 infra/charts/nats/files/headless-service.yaml create mode 100644 infra/charts/nats/files/ingress.yaml create mode 100644 infra/charts/nats/files/nats-box/contents-secret.yaml create mode 100644 infra/charts/nats/files/nats-box/contexts-secret/context.yaml create mode 100644 infra/charts/nats/files/nats-box/contexts-secret/contexts-secret.yaml create mode 100644 infra/charts/nats/files/nats-box/deployment/container.yaml create mode 100644 infra/charts/nats/files/nats-box/deployment/deployment.yaml create mode 100644 infra/charts/nats/files/nats-box/deployment/pod-template.yaml create mode 100644 infra/charts/nats/files/nats-box/service-account.yaml create mode 100644 infra/charts/nats/files/pod-disruption-budget.yaml create mode 100644 infra/charts/nats/files/pod-monitor.yaml create mode 100644 infra/charts/nats/files/service-account.yaml create mode 100644 infra/charts/nats/files/service.yaml create mode 100644 infra/charts/nats/files/stateful-set/jetstream-pvc.yaml create mode 100644 infra/charts/nats/files/stateful-set/nats-container.yaml create mode 100644 infra/charts/nats/files/stateful-set/pod-template.yaml create mode 100644 infra/charts/nats/files/stateful-set/prom-exporter-container.yaml create mode 100644 infra/charts/nats/files/stateful-set/reloader-container.yaml create mode 100644 infra/charts/nats/files/stateful-set/resolver-pvc.yaml create mode 100644 infra/charts/nats/files/stateful-set/stateful-set.yaml create mode 100644 infra/charts/nats/templates/_helpers.tpl create mode 100644 infra/charts/nats/templates/_jsonpatch.tpl create mode 100644 infra/charts/nats/templates/_toPrettyRawJson.tpl create mode 100644 infra/charts/nats/templates/_tplYaml.tpl create mode 100644 infra/charts/nats/templates/config-map.yaml create mode 100644 infra/charts/nats/templates/extra-resources.yaml create mode 100644 infra/charts/nats/templates/headless-service.yaml create mode 100644 infra/charts/nats/templates/ingress.yaml create mode 100644 infra/charts/nats/templates/nats-box/contents-secret.yaml create mode 100644 infra/charts/nats/templates/nats-box/contexts-secret.yaml create mode 100644 infra/charts/nats/templates/nats-box/deployment.yaml create mode 100644 infra/charts/nats/templates/nats-box/service-account.yaml create mode 100644 infra/charts/nats/templates/pod-disruption-budget.yaml create mode 100644 infra/charts/nats/templates/pod-monitor.yaml create mode 100644 infra/charts/nats/templates/service-account.yaml create mode 100644 infra/charts/nats/templates/service.yaml create mode 100644 infra/charts/nats/templates/stateful-set.yaml create mode 100644 infra/charts/nats/templates/tests/request-reply.yaml create mode 100644 infra/charts/nats/values.yaml create mode 100644 infra/kustomization.yaml create mode 100644 infra/nats-cluster.yaml create mode 100644 infra/nats-values.yaml create mode 100644 kustomization.yaml create mode 100644 linkerd/ca.crt create mode 100644 linkerd/ca.key create mode 100644 linkerd/charts/linkerd-control-plane/.helmignore create mode 100644 linkerd/charts/linkerd-control-plane/Chart.lock create mode 100644 linkerd/charts/linkerd-control-plane/Chart.yaml create mode 100644 linkerd/charts/linkerd-control-plane/README.md create mode 100644 linkerd/charts/linkerd-control-plane/README.md.gotmpl create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/.helmignore create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/Chart.yaml create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/README.md create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/README.md.gotmpl create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/templates/NOTES.txt create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/templates/_affinity.tpl create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/templates/_capabilities.tpl create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/templates/_debug.tpl create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/templates/_helpers.tpl create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/templates/_metadata.tpl create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/templates/_network-validator.tpl create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/templates/_nodeselector.tpl create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/templates/_proxy-config-ann.tpl create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/templates/_proxy-init.tpl create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/templates/_proxy.tpl create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/templates/_pull-secrets.tpl create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/templates/_resources.tpl create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/templates/_tolerations.tpl create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/templates/_trace.tpl create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/templates/_validate.tpl create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/templates/_volumes.tpl create mode 100644 linkerd/charts/linkerd-control-plane/charts/partials/values.yaml create mode 100644 linkerd/charts/linkerd-control-plane/templates/NOTES.txt create mode 100644 linkerd/charts/linkerd-control-plane/templates/config-rbac.yaml create mode 100644 linkerd/charts/linkerd-control-plane/templates/config.yaml create mode 100644 linkerd/charts/linkerd-control-plane/templates/destination-rbac.yaml create mode 100644 linkerd/charts/linkerd-control-plane/templates/destination.yaml create mode 100644 linkerd/charts/linkerd-control-plane/templates/heartbeat-rbac.yaml create mode 100644 linkerd/charts/linkerd-control-plane/templates/heartbeat.yaml create mode 100644 linkerd/charts/linkerd-control-plane/templates/identity-rbac.yaml create mode 100644 linkerd/charts/linkerd-control-plane/templates/identity.yaml create mode 100644 linkerd/charts/linkerd-control-plane/templates/namespace.yaml create mode 100644 linkerd/charts/linkerd-control-plane/templates/podmonitor.yaml create mode 100644 linkerd/charts/linkerd-control-plane/templates/proxy-injector-rbac.yaml create mode 100644 linkerd/charts/linkerd-control-plane/templates/proxy-injector.yaml create mode 100644 linkerd/charts/linkerd-control-plane/templates/psp.yaml create mode 100644 linkerd/charts/linkerd-control-plane/values-ha.yaml create mode 100644 linkerd/charts/linkerd-control-plane/values.yaml create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/.helmignore create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/Chart.lock create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/Chart.yaml create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/README.md create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/README.md.gotmpl create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/.helmignore create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/Chart.yaml create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/README.md create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/README.md.gotmpl create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/NOTES.txt create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_affinity.tpl create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_capabilities.tpl create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_debug.tpl create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_helpers.tpl create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_metadata.tpl create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_network-validator.tpl create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_nodeselector.tpl create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_proxy-config-ann.tpl create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_proxy-init.tpl create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_proxy.tpl create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_pull-secrets.tpl create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_resources.tpl create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_tolerations.tpl create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_trace.tpl create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_validate.tpl create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_volumes.tpl create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/values.yaml create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/NOTES.txt create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/gateway.networking.k8s.io_httproutes.yaml create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/authorization-policy.yaml create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/httproute.yaml create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/meshtls-authentication.yaml create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/network-authentication.yaml create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/server-authorization.yaml create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/server.yaml create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/serviceprofile.yaml create mode 100644 linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/values.yaml create mode 100644 linkerd/charts/linkerd-crds/.helmignore create mode 100644 linkerd/charts/linkerd-crds/Chart.lock create mode 100644 linkerd/charts/linkerd-crds/Chart.yaml create mode 100644 linkerd/charts/linkerd-crds/README.md create mode 100644 linkerd/charts/linkerd-crds/README.md.gotmpl create mode 100644 linkerd/charts/linkerd-crds/charts/partials/.helmignore create mode 100644 linkerd/charts/linkerd-crds/charts/partials/Chart.yaml create mode 100644 linkerd/charts/linkerd-crds/charts/partials/README.md create mode 100644 linkerd/charts/linkerd-crds/charts/partials/README.md.gotmpl create mode 100644 linkerd/charts/linkerd-crds/charts/partials/templates/NOTES.txt create mode 100644 linkerd/charts/linkerd-crds/charts/partials/templates/_affinity.tpl create mode 100644 linkerd/charts/linkerd-crds/charts/partials/templates/_capabilities.tpl create mode 100644 linkerd/charts/linkerd-crds/charts/partials/templates/_debug.tpl create mode 100644 linkerd/charts/linkerd-crds/charts/partials/templates/_helpers.tpl create mode 100644 linkerd/charts/linkerd-crds/charts/partials/templates/_metadata.tpl create mode 100644 linkerd/charts/linkerd-crds/charts/partials/templates/_network-validator.tpl create mode 100644 linkerd/charts/linkerd-crds/charts/partials/templates/_nodeselector.tpl create mode 100644 linkerd/charts/linkerd-crds/charts/partials/templates/_proxy-config-ann.tpl create mode 100644 linkerd/charts/linkerd-crds/charts/partials/templates/_proxy-init.tpl create mode 100644 linkerd/charts/linkerd-crds/charts/partials/templates/_proxy.tpl create mode 100644 linkerd/charts/linkerd-crds/charts/partials/templates/_pull-secrets.tpl create mode 100644 linkerd/charts/linkerd-crds/charts/partials/templates/_resources.tpl create mode 100644 linkerd/charts/linkerd-crds/charts/partials/templates/_tolerations.tpl create mode 100644 linkerd/charts/linkerd-crds/charts/partials/templates/_trace.tpl create mode 100644 linkerd/charts/linkerd-crds/charts/partials/templates/_validate.tpl create mode 100644 linkerd/charts/linkerd-crds/charts/partials/templates/_volumes.tpl create mode 100644 linkerd/charts/linkerd-crds/charts/partials/values.yaml create mode 100644 linkerd/charts/linkerd-crds/templates/NOTES.txt create mode 100644 linkerd/charts/linkerd-crds/templates/gateway.networking.k8s.io_httproutes.yaml create mode 100644 linkerd/charts/linkerd-crds/templates/policy/authorization-policy.yaml create mode 100644 linkerd/charts/linkerd-crds/templates/policy/httproute.yaml create mode 100644 linkerd/charts/linkerd-crds/templates/policy/meshtls-authentication.yaml create mode 100644 linkerd/charts/linkerd-crds/templates/policy/network-authentication.yaml create mode 100644 linkerd/charts/linkerd-crds/templates/policy/server-authorization.yaml create mode 100644 linkerd/charts/linkerd-crds/templates/policy/server.yaml create mode 100644 linkerd/charts/linkerd-crds/templates/serviceprofile.yaml create mode 100644 linkerd/charts/linkerd-crds/values.yaml create mode 100644 linkerd/charts/linkerd-viz/.helmignore create mode 100644 linkerd/charts/linkerd-viz/Chart.yaml create mode 100644 linkerd/charts/linkerd-viz/README.md create mode 100644 linkerd/charts/linkerd-viz/README.md.gotmpl create mode 100644 linkerd/charts/linkerd-viz/charts/partials/.helmignore create mode 100644 linkerd/charts/linkerd-viz/charts/partials/Chart.yaml create mode 100644 linkerd/charts/linkerd-viz/charts/partials/README.md create mode 100644 linkerd/charts/linkerd-viz/charts/partials/README.md.gotmpl create mode 100644 linkerd/charts/linkerd-viz/charts/partials/templates/NOTES.txt create mode 100644 linkerd/charts/linkerd-viz/charts/partials/templates/_affinity.tpl create mode 100644 linkerd/charts/linkerd-viz/charts/partials/templates/_capabilities.tpl create mode 100644 linkerd/charts/linkerd-viz/charts/partials/templates/_debug.tpl create mode 100644 linkerd/charts/linkerd-viz/charts/partials/templates/_helpers.tpl create mode 100644 linkerd/charts/linkerd-viz/charts/partials/templates/_metadata.tpl create mode 100644 linkerd/charts/linkerd-viz/charts/partials/templates/_network-validator.tpl create mode 100644 linkerd/charts/linkerd-viz/charts/partials/templates/_nodeselector.tpl create mode 100644 linkerd/charts/linkerd-viz/charts/partials/templates/_proxy-config-ann.tpl create mode 100644 linkerd/charts/linkerd-viz/charts/partials/templates/_proxy-init.tpl create mode 100644 linkerd/charts/linkerd-viz/charts/partials/templates/_proxy.tpl create mode 100644 linkerd/charts/linkerd-viz/charts/partials/templates/_pull-secrets.tpl create mode 100644 linkerd/charts/linkerd-viz/charts/partials/templates/_resources.tpl create mode 100644 linkerd/charts/linkerd-viz/charts/partials/templates/_tolerations.tpl create mode 100644 linkerd/charts/linkerd-viz/charts/partials/templates/_trace.tpl create mode 100644 linkerd/charts/linkerd-viz/charts/partials/templates/_validate.tpl create mode 100644 linkerd/charts/linkerd-viz/charts/partials/templates/_volumes.tpl create mode 100644 linkerd/charts/linkerd-viz/charts/partials/values.yaml create mode 100644 linkerd/charts/linkerd-viz/requirements.lock create mode 100644 linkerd/charts/linkerd-viz/requirements.yaml create mode 100644 linkerd/charts/linkerd-viz/templates/NOTES.txt create mode 100644 linkerd/charts/linkerd-viz/templates/admin-policy.yaml create mode 100644 linkerd/charts/linkerd-viz/templates/metrics-api-policy.yaml create mode 100644 linkerd/charts/linkerd-viz/templates/metrics-api-rbac.yaml create mode 100644 linkerd/charts/linkerd-viz/templates/metrics-api.yaml create mode 100644 linkerd/charts/linkerd-viz/templates/namespace-metadata-rbac.yaml create mode 100644 linkerd/charts/linkerd-viz/templates/namespace-metadata.yaml create mode 100644 linkerd/charts/linkerd-viz/templates/namespace.yaml create mode 100644 linkerd/charts/linkerd-viz/templates/prometheus-policy.yaml create mode 100644 linkerd/charts/linkerd-viz/templates/prometheus-rbac.yaml create mode 100644 linkerd/charts/linkerd-viz/templates/prometheus.yaml create mode 100644 linkerd/charts/linkerd-viz/templates/psp.yaml create mode 100644 linkerd/charts/linkerd-viz/templates/service-profiles.yaml create mode 100644 linkerd/charts/linkerd-viz/templates/tap-injector-policy.yaml create mode 100644 linkerd/charts/linkerd-viz/templates/tap-injector-rbac.yaml create mode 100644 linkerd/charts/linkerd-viz/templates/tap-injector.yaml create mode 100644 linkerd/charts/linkerd-viz/templates/tap-policy.yaml create mode 100644 linkerd/charts/linkerd-viz/templates/tap-rbac.yaml create mode 100644 linkerd/charts/linkerd-viz/templates/tap.yaml create mode 100644 linkerd/charts/linkerd-viz/templates/web-rbac.yaml create mode 100644 linkerd/charts/linkerd-viz/templates/web.yaml create mode 100644 linkerd/charts/linkerd-viz/values-ha.yaml create mode 100644 linkerd/charts/linkerd-viz/values.yaml create mode 100644 linkerd/control-plane.yaml create mode 100644 linkerd/issuer.crt create mode 100644 linkerd/issuer.csr create mode 100644 linkerd/issuer.ext create mode 100644 linkerd/issuer.key create mode 100644 linkerd/kustomization.yaml create mode 100644 linkerd/linkerd-control-plane.yaml create mode 100644 linkerd/linkerd-crds.yaml create mode 100644 linkerd/linkerd-viz.yaml create mode 100644 linkerd/values-identity.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/.helmignore create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/Chart.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/README.md create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/app-readme.md create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/questions.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/NOTES.txt create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/_helpers.tpl create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/clusterrole.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/clusterrolebinding.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/crds.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/daemonset-sa.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/default-setting.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/deployment-driver.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/deployment-ui.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/ingress.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/backing-image-data-source-network-policy.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/backing-image-manager-network-policy.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/instance-manager-networking.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/manager-network-policy.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/recovery-backend-network-policy.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/ui-frontend-network-policy.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/webhook-network-policy.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/postupgrade-job.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/preupgrade-job.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/psp.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/registry-secret.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/serviceaccount.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/services.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/storageclass.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/tls-secrets.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/uninstall-job.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/validate-psp-install.yaml create mode 100644 longhorn-system/charts/longhorn-v1.5.3/longhorn/values.yaml create mode 100644 longhorn-system/kustomization.yaml create mode 100644 longhorn-system/longhorn-ingress.yaml create mode 100644 longhorn-system/values.yaml create mode 100644 manuals.md create mode 100644 manuals/deploy_k8s_components.md create mode 100644 manuals/important_commands.md create mode 100644 manuals/interconnection.md create mode 100644 manuals/k8s_yaml.md create mode 100644 manuals/setup_k8s.md create mode 100644 namespaces.yaml create mode 100644 namespaces/apps-ns.yaml create mode 100644 namespaces/argocd-ns.yaml create mode 100644 namespaces/db-ns.yaml create mode 100644 namespaces/infra-ns.yaml create mode 100644 namespaces/kustomization.yaml create mode 100644 namespaces/stream-ns.yaml create mode 100644 setup-local-tools.yml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/.helmignore create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/Chart.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/README.md create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/app-readme.md create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/expose-all-shared.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/expose-all.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/generate_kubeconfig.py create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/openshift.values.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/questions.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/NOTES.txt create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/_helpers.tpl create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/certificates.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/controller-gflags-secret.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/debug_config_map.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/hooks/setup-credentials-job.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/master-gflags-secret.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/master-servicemonitor.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/multicluster/common-tserver-service.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/multicluster/mcs-service-export.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/multicluster/service-per-pod.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/otel-collector.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/secrets.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/service-endpoints.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/service.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/setup-credentials-configmap.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/tserver-gflags-secret.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/tserver-servicemonitor.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/values.yaml create mode 100644 yugabyte/charts/yugabyte-2025.2.1/yugabyte/yugabyte-rbac.yaml create mode 100644 yugabyte/kustomization.yaml create mode 100644 yugabyte/yugabytedb.yaml create mode 100644 yugabyte/yugabytedb2.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/ansible-k3s/ansible.cfg b/ansible-k3s/ansible.cfg new file mode 100644 index 0000000..382519d --- /dev/null +++ b/ansible-k3s/ansible.cfg @@ -0,0 +1,3 @@ +[defaults] +host_key_checking = False +inventory = hosts.ini \ No newline at end of file diff --git a/ansible-k3s/hosts.ini b/ansible-k3s/hosts.ini new file mode 100644 index 0000000..d9a6f06 --- /dev/null +++ b/ansible-k3s/hosts.ini @@ -0,0 +1,37 @@ +; [all:vars] +; ansible_user=ubuntu-s # using same user for all VMs +; # If you use the SSH key method above: +; # ansible_ssh_private_key_file=~/.ssh/id_rsa +; # using mounted private key from windows folder structure from does not work +; #ansible_ssh_private_key_file=/mnt/c/Users/wendg2/.ssh/id_ed25519 +; ansible_ssh_private_key_file=~/.ssh/id_ed25519 +; [master] +; 192.168.3.91 + +; [workers] +; 192.168.3.93 +; 192.168.3.92 +; 192.168.3.94 +[master] +192.168.3.91 ansible_user=master-1 + +# different user name +[dbworkers] +192.168.3.92 ansible_user=master-2 +192.168.3.93 ansible_user=master-3 +192.168.3.94 ansible_user=master-4 + +[workers] +192.168.3.95 ansible_user=master-5 +192.168.3.96 ansible_user=master-6 +# This group combines both for easy targeting +[k8s_nodes:children] +master +workers + + +# [k8s_nodes:vars] +# # Ensure Ansible uses the correct Python on the remote nodes +# ansible_python_interpreter=/usr/bin/python3 +# # If you use the same SSH key for all: +# ansible_ssh_private_key_file=~/.ssh/id_rsa \ No newline at end of file diff --git a/ansible-k3s/k3s-remote-control.yaml b/ansible-k3s/k3s-remote-control.yaml new file mode 100644 index 0000000..327d04b --- /dev/null +++ b/ansible-k3s/k3s-remote-control.yaml @@ -0,0 +1,61 @@ +--- +# - name: Setup Local Laptop Tools +# hosts: localhost +# connection: local +# become: yes +# tasks: +# - name: Set ANSIBLE_CONFIG locally +# become: yes +# ansible.builtin.lineinfile: +# path: "~/.bashrc" +# line: 'export ANSIBLE_CONFIG="/mnt/f/Invixel/k8s-infra-cluster/ansible.cfg"' +# state: present + +- name: Setup Laptop to Control K3s Cluster + hosts: master + become: yes + + vars: + # Replace this with your Master's actual LAN or Tailscale IP + master_public_ip: "192.168.3.91" + local_kube_path: "~/.kube/config" + + tasks: + - name: Ensure K3s is running with TLS SAN for the Public IP + ansible.builtin.shell: | + curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--tls-san {{ master_public_ip }}" sh - + register: k3s_install + - name: Wait for k3s.yaml to be fully generated + ansible.builtin.wait_for: + path: /etc/rancher/k3s/k3s.yaml + search_regex: "certificate-authority-data" # Ensures the file isn't empty + timeout: 30 + + # - name: Read k3s.yaml as plain text + # ansible.builtin.command: cat /etc/rancher/k3s/k3s.yaml + # register: k3s_config_raw + # changed_when: false + + # - name: Prepare and save config to local laptop + # delegate_to: localhost + # become: no + # ansible.builtin.copy: + # # We use .stdout here because that's where the 'cat' text is stored + # content: "{{ k3s_config_raw.stdout | replace('127.0.0.1', master_public_ip) }}" + # dest: "{{ local_kube_path }}" + # mode: '0600' + + - name: Read k3s.yaml from master + ansible.builtin.slurp: + src: /etc/rancher/k3s/k3s.yaml + register: k3s_config_encoded + + - name: Prepare and save config to local laptop + delegate_to: localhost + become: no + ansible.builtin.copy: + content: "{{ k3s_config_encoded.content | b64decode | replace('127.0.0.1', master_public_ip) }}" + dest: "{{ local_kube_path }}" + mode: '0600' + +# ansible-playbook -i hosts.ini k3s-remote-control.yml \ No newline at end of file diff --git a/ansible-prepare-k3s.yml b/ansible-prepare-k3s.yml new file mode 100644 index 0000000..6af4ad5 --- /dev/null +++ b/ansible-prepare-k3s.yml @@ -0,0 +1,32 @@ +--- +- name: Prepare Nodes for Longhorn + hosts: k8s_nodes + become: yes + tasks: + - name: Install prerequisites + apt: + name: [open-iscsi, nfs-common, util-linux] + state: present + update_cache: yes + + - name: Enable and start iscsid + systemd: + name: iscsid + enabled: yes + state: started + # - name: Deploy Infra and Router via Kustomize + # become: false + # shell: "kustomize build --enable-helm . | kubectl apply --server-side --force-conflicts -f -" + # args: + # chdir: "/mnt/f/Invixel/k8s-infra-cluster" + + # - name: Install Cosmo Router via Helm + # become: false + # shell: "helm upgrade --install cosmo-router ./cosmo-router -n apps --create-namespace" + # args: + # chdir: "/mnt/f/Invixel/k8s-infra-cluster" + + + +# ansible-playbook -i hosts.ini ansible-prepare-k3s.yml +# ansible k8s_nodes -i hosts.ini -m ping diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..382519d --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,3 @@ +[defaults] +host_key_checking = False +inventory = hosts.ini \ No newline at end of file diff --git a/argocd/argocd-manifest.yaml b/argocd/argocd-manifest.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2ee53b43715617554806637949973b045f109a39 GIT binary patch literal 3881588 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 ~#Ot^g*bolw)K&K> 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%0Atw27FK78a5xfF= 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 item.sku) + + - log: + level: INFO + message: "New transaction: status ${! meta(\"transaction_status\") } - order ID: ${! meta(\"order_id\") } timestamp: ${! json(\"timestamp\")} timestamp2a ${! this.timestamp }" + +# output: +# broker: +# pattern: fan_out +# outputs: +# # --- Branch 1: Archive the raw JSON --- +# - yugabyte_insert: +# dsn: "postgres://yugabyte@yugabytedb-3:5433/ecommerce?sslmode=disable" +# table_name: "public.transaction" +# #conflict_columns: "transaction_id" +# #columns: ["transaction_id", "orders", "order_status"] +# # if use this deactivate conflict_columns +# columns: ["content_hash", "orders", "status","created_at"] +# value_mapping: | +# root = [ +# # if use meta define it first above +# # meta("transaction_id"), +# # meta("order_id"), +# # meta("order_status") +# this.transaction_id, +# this.data.order.id, +# this.transaction_status, +# this.timestamp +# ] + +# # --- Branch 2: Extract nested GraphQL data into Facts --- + +# - yugabyte_insert: +# dsn: "postgres://yugabyte@yugabytedb-3:5433/ecommerce?sslmode=disable" +# table_name: "public.order" +# # conflict_columns: "content_hash" +# columns: ["content_hash", "customer_email", "customer_loyalty_tier","status", "ip_address","region"] +# # if use this deactivate conflict_columns +# #columns: ["content_hash", "customer_email", "status"] +# value_mapping: | +# root = [ +# # if use meta define it first above +# # meta("order_id"), +# # meta("order_customer_email"), +# # meta("order_status") +# this.data.order.id, +# this.data.order.customer.email, +# this.data.order.customer.loyalty_tier, +# this.data.order.status, +# this.data.order.metadata.ip_address, +# this.data.order.metadata.region +# ] + +# - yugabyte_insert: +# dsn: "postgres://yugabyte@yugabytedb-3:5433/ecommerce?sslmode=disable" +# table_name: "public.items" +# columns: ["content_hash", "items"] +# # No unarchive needed! +# processors: +# - mapping: | +# root = this +# # 1. Update dedupe key for this specific table +# meta conflict_col = meta("order_id") + "-item" + +# # 2. Stringify the items array for the JSONB column +# # root.items_blob = this.data.order.items.format_json() + +# value_mapping: | +# root = [ +# meta("order_id"), +# this.data.order.items.string() +# ] + +# - kafka: +# addresses: [ "redpanda:9092" ] +# topic: "transactions_redpanda" +# # By not adding a 'processors' block here, +# # it sends the current state of 'root' from the global pipeline. +# # To ensure it is the ABSOLUTE original, we can force a mapping: +# key : ${! meta("transaction_id")} +# processors: +# - mapping: | +# root = content() + +# 1. example below for using content_hash +# 2. change it accordingly to your defined conflict_columns +# 3. IMPORTANT: conflict_columns is to be defined as UNIQUE in sql +# CREATE TABLE public.transaction ( +# id BIGSERIAL, +# content_hash TEXT UNIQUE, +# orders TEXT, +# order_status TEXT, +# timestamp TIMESTAMPTZ, +# PRIMARY KEY (id) +# ); + +# CREATE TABLE public.order ( +# id TEXT PRIMARY KEY, -- Must be TEXT for "ORD-5521" +# content_hash TEXT UNIQUE, +# customer_email TEXT, +# amount DECIMAL +# ); \ No newline at end of file diff --git a/infra/benthos/streams/poll_request_log_id_serial.yaml b/infra/benthos/streams/poll_request_log_id_serial.yaml new file mode 100644 index 0000000..8bd2618 --- /dev/null +++ b/infra/benthos/streams/poll_request_log_id_serial.yaml @@ -0,0 +1,28 @@ +logger: + level: DEBUG # Force Benthos to tell us everything it's doing + format: logfmt +# This tells Benthos: Use the 'sql_poller' template as the input +input: + sql_poller: + dsn: "postgres://invixel_admin:${ECOM_PASS}@postgres-ha-rw.db.svc.cluster.local:5432/ecommerce?sslmode=disable" + + #stream: "fact_stream" + #subject: "output_request_logs" + #dsn: "postgres://yugabyte@yugabytedb-2:5433/ecommerce?sslmode=disable" + table_name: "public.request_logs" + id_column: "id" # Or 'id' if you use the hashed string + +pipeline: + processors: + - log: + level: INFO + message: "Polled a message: ${! content() }" + +# The output sends the polled data back to NATS +output: + nats_jetstream: + #urls: [ "nats:4222" ] + urls: [ "nats://nats-cluster.infra.svc.cluster.local:4222" ] + subject: "output_request_logs" + stream: "fact_stream" + #manage_stream: true # This is the key setting \ No newline at end of file diff --git a/infra/benthos/streams/poll_transaction_id_serial.yaml b/infra/benthos/streams/poll_transaction_id_serial.yaml new file mode 100644 index 0000000..b888d70 --- /dev/null +++ b/infra/benthos/streams/poll_transaction_id_serial.yaml @@ -0,0 +1,28 @@ +# This tells Benthos: Use the 'sql_poller' template as the input +input: + sql_poller: + dsn: "postgres://invixel_admin:${ECOM_PASS}@postgres-ha-rw.db.svc.cluster.local:5432/ecommerce?sslmode=allow" + + #dsn: "postgres://yugabyte@yugabytedb-2:5433/ecommerce?sslmode=disable" + table_name: "transaction" + id_column: "id" # Or 'id' if you use the hashed string + +# pipeline: +# processors: +# - mapping: | +# root = this +# # Set metadata globally so all outputs and templates can see it +# root.transaction_id = this.transaction_id.or("unknown") + + +# - log: +# level: INFO +# message: "2b.New transaction: status - order ID: ${! meta(\"order_id\") } timestamp: ${! json(\"timestamp\")} timestamp3 ${! timestamp }" + +# The output sends the polled data back to NATS +# output: +# nats_jetstream: +# urls: [ "nats:4222" ] +# subject: "output_transaction" +# stream: "fact_stream" +# manage_stream: true # This is the key setting \ No newline at end of file diff --git a/infra/benthos/streams/reserve/ingest_request_log_id_hash_received_at.yaml b/infra/benthos/streams/reserve/ingest_request_log_id_hash_received_at.yaml new file mode 100644 index 0000000..7b6cbbd --- /dev/null +++ b/infra/benthos/streams/reserve/ingest_request_log_id_hash_received_at.yaml @@ -0,0 +1,26 @@ +# streams/request_logs.yaml +input: + nats_jetstream: + urls: [ "nats:4222" ] + subject: "input_request_logs" + +output: + yugabyte_insert: + dsn: "postgres://yugabyte@yugabytedb-3:5433/yugabyte?sslmode=disable" + table: "public.request_logs" + + # 1. Define the columns for THIS specific table + columns: ["id", "payload", "received_at"] + + # 2. Define how to map data to those columns + value_mapping: | + root = [ + content().hash("sha256").encode("hex"), + this.string(), + now() + ] +# CREATE TABLE request_logs ( +# id TEXT PRIMARY KEY, -- This will store our SHA-256 hash +# payload JSONB, -- Stores the full original message +# received_at TIMESTAMPTZ -- Timestamp for auditing +# ); \ No newline at end of file diff --git a/infra/benthos/streams/reserve/poll_request_log_received_at.yaml b/infra/benthos/streams/reserve/poll_request_log_received_at.yaml new file mode 100644 index 0000000..d9c1231 --- /dev/null +++ b/infra/benthos/streams/reserve/poll_request_log_received_at.yaml @@ -0,0 +1,14 @@ +# This tells Benthos: Use the 'sql_poller' template as the input +input: + sql_poller: + dsn: "postgres://yugabyte@yugabytedb-2:5433/yugabyte?sslmode=disable" + table_name: "request_logs" + id_column: "received_at" # Or 'id' if you use the hashed string + +# The output sends the polled data back to NATS +# output: +# nats_jetstream: +# urls: [ "nats:4222" ] +# subject: "public.request_logs" +# stream: "fact_stream" +# manage_stream: true # This is the key setting \ No newline at end of file diff --git a/infra/benthos/streams/upsert_user.yaml b/infra/benthos/streams/upsert_user.yaml new file mode 100644 index 0000000..ad49f23 --- /dev/null +++ b/infra/benthos/streams/upsert_user.yaml @@ -0,0 +1,13 @@ +input: + nats_jetstream: + urls: ["nats://nats:4222"] + subject: "user.updates" + stream: "request_stream" + +pipeline: + processors: + - mapping: | + root.id = this.id + root.username = this.username + root.email = this.email + root.updated_at = now() \ No newline at end of file diff --git a/infra/benthos/templates/ingest_template_id_serial.yaml b/infra/benthos/templates/ingest_template_id_serial.yaml new file mode 100644 index 0000000..0913985 --- /dev/null +++ b/infra/benthos/templates/ingest_template_id_serial.yaml @@ -0,0 +1,76 @@ +# templates/sql_ingest.yaml +name: yugabyte_insert +type: output + +fields: + - name: table_name + type: string + - name: dsn + type: string + - name: columns + type: unknown + - name: value_mapping + type: string + - name: conflict_columns # NEW: Make it flexible! + type: string + default: "content_hash" + +mapping: | + # root.processors = [ + # # LAYER 2: Local Bloom-style Gate (RAM) + # { + # "dedupe": { + # "cache": "local_bloom_gate", + # # "key": "${! content().hash(\"sha256\").encode(\"hex\") }", + # # without concatenating with this.table name inserting multiple tables not possible + # "key": "${! meta(\"conflict_col\") }-" + this.table_name , + # "drop_on_err": false + # } + # }, + # # VISIBILITY: Log what survived the Bloom Filter + # { + # "log": { + # "level": "INFO", + # "message": "Passed Bloom Filter: transaction ${! meta(\"conflict_col\") }" + # } + # }, + # # LAYER 3 Redis Global Gate (Network) + # { + # "dedupe": { + # "cache": "dedupe_cache", + # # "key": "${! content().hash(\"sha256\").encode(\"hex\") }", + # "key": "${! meta(\"conflict_col\") }-" + this.table_name , + # "drop_on_err": false + # } + # }, + # # NEW: Add Global Logging/Error Handling for ALL inserts using this template + # { + # "catch": [ + # { + # "log": { + # "level": "ERROR", + # "message": "SQL Error on " + this.table_name + ": ${! error() }" + # } + # } + # ] + # }, + # # B. Success Logging + # { + # "log": { + # "level": "INFO", + # "message": "Attempting insert for " + this.table_name + " ID: ${! json(\"data.order.id\") }" + # } + # } + + # ] + + root.sql_insert = { + "driver": "postgres", + "dsn": this.dsn, + "table": this.table_name, + "columns": this.columns, + # FIX: We use 'this.value_mapping' directly so the stream's logic is used + "args_mapping": this.value_mapping, + "suffix": "ON CONFLICT ( "+ this.conflict_columns + ") DO NOTHING" + #"suffix": "ON CONFLICT (content_hash) DO NOTHING" + } diff --git a/infra/benthos/templates/poll_template_id_serial.yaml b/infra/benthos/templates/poll_template_id_serial.yaml new file mode 100644 index 0000000..23926ea --- /dev/null +++ b/infra/benthos/templates/poll_template_id_serial.yaml @@ -0,0 +1,59 @@ +# templates/poll_template.yaml +name: sql_poller +type: input + +# using BIGSERIAL +fields: + - name: table_name + type: string + - name: dsn + type: string + - name: id_column + type: string + default: "id" + +mapping: | + # 1. The base input is a generator that ticks every 2s + root.generate.interval = "2s" + root.generate.mapping = "root = {}" + + # 2. We attach the processing logic directly to this input + root.processors = [ + { + "cache": { + "resource": "yugabyte_checkpoints", + "operator": "get", + "key": "last_id_" + this.table_name + } + }, + { "catch": [ { "mapping": "root = \"0\"" } ] }, # Default to 0 if cache missing + { "mapping": "meta last_id = content().string()" }, + + # 3. Perform the Query + { + "sql_raw": { + "driver": "postgres", + "dsn": this.dsn, + "query": "SELECT * FROM " + this.table_name + " WHERE " + this.id_column + " > $1 ORDER BY " + this.id_column + " ASC LIMIT 500;", + "args_mapping": "root = [ meta(\"last_id\").number() ]" + } + }, + + # 4. Filter empty results + { + "mapping": "root = if this.type() != \"array\" || this.length() == 0 { deleted() } else { this }" + }, + + # 5. Update the Cache with the new max ID + { + "branch": { + "processors": [ + { "mapping": "root = this.index(-1)." + this.id_column + ".string()" }, + { "cache": { "resource": "yugabyte_checkpoints", "operator": "set", "key": "last_id_" + this.table_name, "value": "${! content() }" }} + ] + } + }, + + # 6. Flatten the array into individual messages + { "unarchive": { "format": "json_array" } } + ] diff --git a/infra/benthos/templates/reserve/ingest_template_id_hash_received_at.yaml b/infra/benthos/templates/reserve/ingest_template_id_hash_received_at.yaml new file mode 100644 index 0000000..7f89e73 --- /dev/null +++ b/infra/benthos/templates/reserve/ingest_template_id_hash_received_at.yaml @@ -0,0 +1,28 @@ +# templates/ingest_template.yaml +name: yugabyte_insert +type: output + +fields: + - name: dsn + type: string + - name: table + type: string + - name: columns + type: unknown + description: "List of column names for this table" + - name: value_mapping + type: string + description: "Bloblang mapping that generates the array of values matching the columns" + +mapping: | + root.sql_insert.driver = "postgres" + root.sql_insert.dsn = this.dsn + root.sql_insert.table = this.table + root.sql_insert.columns = this.columns + + # We inject the specific mapping logic passed from the config + root.sql_insert.args_mapping = this.value_mapping + + # Standardize your conflict handling here + root.sql_insert.suffix = "ON CONFLICT (id) DO NOTHING" + diff --git a/infra/benthos/templates/reserve/poll_template_id_hash_received_at.yaml b/infra/benthos/templates/reserve/poll_template_id_hash_received_at.yaml new file mode 100644 index 0000000..100d4ea --- /dev/null +++ b/infra/benthos/templates/reserve/poll_template_id_hash_received_at.yaml @@ -0,0 +1,136 @@ +# templates/poll_template.yaml +name: sql_poller +type: input + +# using received_at +fields: + - name: table_name + type: string + - name: dsn + type: string + - name: id_column + type: string + default: "received_at" + +mapping: | + root.generate.interval = "2s" + root.generate.mapping = "root = {}" + + root.processors = [ + # 1. Fetch from cache. If it fails, we catch it inside the mapping. + { + "branch": { + "processors": [ + { "try": [ { "cache": { "resource": "yugabyte_checkpoints", "operator": "get", "key": "last_ts_" + this.table_name } } ] } + ], + "catch": [ { "mapping": "root = \"1970-01-01T00:00:00Z\"" } ], + "result_map": "meta last_ts = content().string()" + } + }, + # 2. Hard-set a default if metadata is STILL null/empty + { + "mapping": """ + meta last_ts = if meta("last_ts") == null || meta("last_ts") == "" { + "1970-01-01T00:00:00Z" + } else { + meta("last_ts") + } + """ + }, + + # DEBUG: Log the timestamp being used + { "log": { "level": "INFO", "message": "Polling " + this.table_name + " with TS: ${! meta(\"last_ts\") }" } }, + + # 3. Query Yugabyte + { + "sql_raw": { + "driver": "postgres", + "dsn": this.dsn, + "query": "SELECT * FROM " + this.table_name + " WHERE " + this.id_column + " >= ($1::timestamptz - INTERVAL '5 seconds') ORDER BY " + this.id_column + " ASC LIMIT 500;", + "args_mapping": "root = [ meta(\"last_ts\") ]" + } + }, + { "catch": [ { "log": { "level": "ERROR", "message": "SQL Error: ${! error() }" } } ] }, + + # 3. Check if results exist + { + "mapping": "root = if this.type() != \"array\" || this.length() == 0 { deleted() } else { this }" + }, + + # 4. Save the NEW checkpoint + { + "branch": { + "processors": [ + { "mapping": "root = this.index(-1)." + this.id_column + ".string()" }, + { "log": { "level": "INFO", "message": "Saving new checkpoint for " + this.table_name + ": ${! content() }" } }, + { + "cache": { + "resource": "yugabyte_checkpoints", + "operator": "set", + "key": "last_ts_" + this.table_name, + "value": "${! content() }" + } + } + ] + } + }, + + # 5. Fan out + { "unarchive": { "format": "json_array" } } + ] + +# # using BIGSERIAL +# fields: +# - name: table_name +# type: string +# - name: dsn +# type: string +# - name: id_column +# type: string +# default: "id" + +# mapping: | +# # 1. The base input is a generator that ticks every 2s +# root.generate.interval = "2s" +# root.generate.mapping = "root = {}" + +# # 2. We attach the processing logic directly to this input +# root.processors = [ +# { +# "cache": { +# "resource": "state_cache", +# "operator": "get", +# "key": "last_id_" + this.table_name +# } +# }, +# { "catch": [ { "mapping": "root = \"0\"" } ] }, # Default to 0 if cache missing +# { "mapping": "meta last_id = content().string()" }, + +# # 3. Perform the Query +# { +# "sql_raw": { +# "driver": "postgres", +# "dsn": this.dsn, +# "query": "SELECT * FROM " + this.table_name + " WHERE " + this.id_column + " > $1 ORDER BY " + this.id_column + " ASC LIMIT 500;", +# "args_mapping": "root = [ meta(\"last_id\") ]" +# } +# }, + +# # 4. Filter empty results +# { +# "mapping": "root = if this.type() != \"array\" || this.length() == 0 { deleted() } else { this }" +# }, + +# # 5. Update the Cache with the new max ID +# { +# "branch": { +# "processors": [ +# { "mapping": "root = this.index(-1)." + this.id_column + ".string()" }, +# { "cache": { "resource": "state_cache", "operator": "set", "key": "last_id_" + this.table_name, "value": "${! content() }" }} +# ] +# } +# }, + +# # 6. Flatten the array into individual messages +# { "unarchive": { "format": "json_array" } } +# ] diff --git a/infra/charts/nats/.helmignore b/infra/charts/nats/.helmignore new file mode 100644 index 0000000..240dfde --- /dev/null +++ b/infra/charts/nats/.helmignore @@ -0,0 +1,26 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ + +# template tests +/test diff --git a/infra/charts/nats/Chart.yaml b/infra/charts/nats/Chart.yaml new file mode 100644 index 0000000..cef47d1 --- /dev/null +++ b/infra/charts/nats/Chart.yaml @@ -0,0 +1,16 @@ +apiVersion: v2 +appVersion: 2.12.5 +description: A Helm chart for the NATS.io High Speed Cloud Native Distributed Communications + Technology. +home: http://github.com/nats-io/k8s +icon: https://nats.io/img/nats-icon-color.png +keywords: +- nats +- messaging +- cncf +maintainers: +- email: info@nats.io + name: The NATS Authors + url: https://github.com/nats-io +name: nats +version: 2.12.5 diff --git a/infra/charts/nats/README.md b/infra/charts/nats/README.md new file mode 100644 index 0000000..d27d02b --- /dev/null +++ b/infra/charts/nats/README.md @@ -0,0 +1,354 @@ +# NATS Server + +--- + +[NATS](https://nats.io) is a simple, secure and performant communications system for digital systems, services and devices. +NATS is part of the Cloud Native Computing Foundation ([CNCF](https://cncf.io)). +NATS has over [30 client language implementations](https://nats.io/download/), and its server can run on-premise, in the cloud, at the edge, and even on a Raspberry Pi. +NATS can secure and simplify design and operation of modern distributed systems. + +```shell +helm repo add nats https://nats-io.github.io/k8s/helm/charts/ +helm upgrade --install nats nats/nats +``` + +## Upgrade Nodes + +- **Upgrading from 0.x**: The `values.yaml` schema changed significantly from 0.x to 1.x. Read [UPGRADING.md](UPGRADING.md) for instructions on upgrading a 0.x release to 1.x. + +## Values + +There are a handful of explicitly defined options which are documented with comments in the [values.yaml](values.yaml) file. + +Everything in the NATS Config or Kubernetes Resources can be overridden by `merge` and `patch`, which is supported for the following values: + +| key | type | enabled by default | +|----------------------------------|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------| +| `config` | [NATS Config](https://docs.nats.io/running-a-nats-service/configuration) | yes | +| `config.cluster` | [NATS Cluster](https://docs.nats.io/running-a-nats-service/configuration/clustering/cluster_config) | no | +| `config.cluster.tls` | [NATS TLS](https://docs.nats.io/running-a-nats-service/configuration/securing_nats/tls) | no | +| `config.jetstream` | [NATS JetStream](https://docs.nats.io/running-a-nats-service/configuration#jetstream) | no | +| `config.jetstream.fileStore.pvc` | [k8s PVC](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#persistentvolumeclaim-v1-core) | yes, when `config.jetstream` is enabled | +| `config.nats.tls` | [NATS TLS](https://docs.nats.io/running-a-nats-service/configuration/securing_nats/tls) | no | +| `config.leafnodes` | [NATS LeafNodes](https://docs.nats.io/running-a-nats-service/configuration/leafnodes/leafnode_conf) | no | +| `config.leafnodes.tls` | [NATS TLS](https://docs.nats.io/running-a-nats-service/configuration/securing_nats/tls) | no | +| `config.websocket` | [NATS WebSocket](https://docs.nats.io/running-a-nats-service/configuration/websocket/websocket_conf) | no | +| `config.websocket.tls` | [NATS TLS](https://docs.nats.io/running-a-nats-service/configuration/securing_nats/tls) | no | +| `config.websocket.ingress` | [k8s Ingress](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#ingress-v1-networking-k8s-io) | no | +| `config.mqtt` | [NATS MQTT](https://docs.nats.io/running-a-nats-service/configuration/mqtt/mqtt_config) | no | +| `config.mqtt.tls` | [NATS TLS](https://docs.nats.io/running-a-nats-service/configuration/securing_nats/tls) | no | +| `config.gateway` | [NATS Gateway](https://docs.nats.io/running-a-nats-service/configuration/gateways/gateway#gateway-configuration-block) | no | +| `config.gateway.tls` | [NATS TLS](https://docs.nats.io/running-a-nats-service/configuration/securing_nats/tls) | no | +| `config.resolver` | [NATS Resolver](https://docs.nats.io/running-a-nats-service/configuration/securing_nats/auth_intro/jwt/resolver) | no | +| `config.resolver.pvc` | [k8s PVC](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#persistentvolumeclaim-v1-core) | yes, when `config.resolver` is enabled | +| `container` | nats [k8s Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#container-v1-core) | yes | +| `reloader` | config reloader [k8s Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#container-v1-core) | yes | +| `promExporter` | prometheus exporter [k8s Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#container-v1-core) | no | +| `promExporter.podMonitor` | [prometheus PodMonitor](https://prometheus-operator.dev/docs/api-reference/api/#monitoring.coreos.com/v1.PodMonitor) | no | +| `service` | [k8s Service](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#service-v1-core) | yes | +| `statefulSet` | [k8s StatefulSet](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#statefulset-v1-apps) | yes | +| `podTemplate` | [k8s PodTemplate](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#pod-v1-core) | yes | +| `headlessService` | [k8s Service](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#service-v1-core) | yes | +| `configMap` | [k8s ConfigMap](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#configmap-v1-core) | yes | +| `natsBox.contexts.default` | [NATS Context](https://docs.nats.io/using-nats/nats-tools/nats_cli#nats-contexts) | yes | +| `natsBox.contexts.[name]` | [NATS Context](https://docs.nats.io/using-nats/nats-tools/nats_cli#nats-contexts) | no | +| `natsBox.container` | nats-box [k8s Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#container-v1-core) | yes | +| `natsBox.deployment` | [k8s Deployment](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#deployment-v1-apps) | yes | +| `natsBox.podTemplate` | [k8s PodTemplate](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#pod-v1-core) | yes | +| `natsBox.contextsSecret` | [k8s Secret](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#secret-v1-core) | yes | +| `natsBox.contentsSecret` | [k8s Secret](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#secret-v1-core) | yes | + +### Merge + +Merging is performed using the Helm [`merge` function](https://helm.sh/docs/chart_template_guide/function_list/#merge-mustmerge). Example - add NATS accounts and container resources: + +```yaml +config: + merge: + accounts: + A: + users: + - {user: a, password: a} + B: + users: + - {user: b, password: b} +natsBox: + contexts: + a: + merge: {user: a, password: a} + b: + merge: {user: b, password: b} + defaultContextName: a +``` + +## Patch + +Patching is performed using [JSON Patch](https://jsonpatch.com/). Example - add additional route to end of route list: + +```yaml +config: + cluster: + enabled: true + patch: + - op: add + path: /routes/- + value: nats://demo.nats.io:6222 +``` + +## Common Configurations + +### JetStream Cluster on 3 separate hosts + +```yaml +config: + cluster: + enabled: true + replicas: 3 + jetstream: + enabled: true + fileStore: + pvc: + size: 10Gi + +podTemplate: + topologySpreadConstraints: + kubernetes.io/hostname: + maxSkew: 1 + whenUnsatisfiable: DoNotSchedule +``` + +### NATS Container Resources + +We recommend setting both **requests and limits** - for both **CPU and memory** - **to the same value** for the following reasons: + +* It ensures your NATS pod has [predictable performance](https://www.datadoghq.com/blog/kubernetes-cpu-requests-limits/#predictability:~:text=If%20containers%20are,available%20capacity%20decreases.). +* The Go runtime [automatically sets](https://go.dev/doc/go1.25#container-aware-gomaxprocs) [GOMAXPROCS](https://pkg.go.dev/runtime#GOMAXPROCS) to the number of CPU cores defined in the `limits` section. If `limits` are not set, GOMAXPROCS defaults to the node's physical core count, which can lead to [poor performance](https://github.com/golang/go/issues/33803). +* The pod will be assigned to the ["Guaranteed" QoS class](https://kubernetes.io/docs/concepts/workloads/pods/pod-qos/#guaranteed), making it less likely to be evicted when node resources are constrained. +* When deciding how much CPU time to dedicate to Garbage Collection, the Go Runtime assumes that it has access to `GOMAXPROCS*N` seconds of CPU time in `N` second of wall time. [It can cause issues, if this assumption is not true.](https://github.com/golang/go/issues/59715) + +Deviate from this recommendation only if you fully understand the implications of your settings. + +```yaml +container: + env: + # Different from k8s units, suffix must be B, KiB, MiB, GiB, or TiB + # Should be ~80% of memory limit + GOMEMLIMIT: 6GiB + merge: + # Recommended minimum: at least 2 CPU cores and 8Gi memory for production JetStream clusters + resources: + requests: + cpu: "2" + memory: 8Gi + limits: + cpu: "2" + memory: 8Gi +``` + +### Specify Image Version + +The container image can now be overridden by specifying either the image tag, an image digest, or a full image name. Examples below illustrate the options: + +- To set the tag: + ```yaml + container: + image: + tag: x.y.z-alpine + ``` +- To use an image digest, which overrides the tag: + ```yaml + container: + image: + repository: nats + digest: sha256:abcdef1234567890... + ``` +- To override the registry, repository, tag, and digest all at once, specify a full image name: + ```yaml + container: + image: + fullImageName: custom-reg.io/myimage@sha256:abcdef1234567890... + ``` + +### Operator Mode with NATS Resolver + +Run `nsc generate config --nats-resolver` and replace the `OPERATOR_JWT`, `SYS_ACCOUNT_ID`, and `SYS_ACCOUNT_JWT` with your values. +Make sure that you do not include the trailing `,` in the `SYS_ACCOUNT_JWT`. + +``` +config: + resolver: + enabled: true + merge: + type: full + interval: 2m + timeout: 1.9s + merge: + operator: OPERATOR_JWT + system_account: SYS_ACCOUNT_ID + resolver_preload: + SYS_ACCOUNT_ID: SYS_ACCOUNT_JWT +``` + + +## Accessing NATS + +The chart contains 2 services by default, `service` and `headlessService`. + +### `service` + +The `service` is intended to be accessed by NATS Clients. It is a `ClusterIP` service by default, however it can easily be changed to a different service type. + +The `nats`, `websocket`, `leafnodes`, and `mqtt` ports will be exposed through this service by default if they are enabled. + +Example: change this service type to a `LoadBalancer`: + +```yaml +service: + merge: + spec: + type: LoadBalancer +``` + +### `headlessService` + +The `headlessService` is used for NATS Servers in the Stateful Set to discover one another. It is primarily intended to be used for Cluster Route connections. + +### TLS Considerations + +The TLS Certificate used for Client Connections should have a SAN covering DNS Name that clients access the `service` at. + +The TLS Certificate used for Cluster Route Connections should have a SAN covering the DNS Name that routes access each other on the `headlessService` at. This is `*.` by default. + +## Advanced Features + +### Templating Values + +Anything in `values.yaml` can be templated: + +- maps matching the following syntax will be templated and parsed as YAML: + ```yaml + $tplYaml: | + yaml template + ``` +- maps matching the follow syntax will be templated, parsed as YAML, and spread into the parent map/slice + ```yaml + $tplYamlSpread: | + yaml template + ``` + +Example - change service name: + +```yaml +service: + name: + $tplYaml: >- + {{ include "nats.fullname" . }}-svc +``` + +### NATS Config Units and Variables + +NATS configuration extends JSON, and can represent Units and Variables. They must be wrapped in `<< >>` in order to template correctly. Example: + +```yaml +config: + merge: + authorization: + # variable + token: << $TOKEN >> + # units + max_payload: << 2MB >> +``` + +templates to the `nats.conf`: + +``` +{ + "authorization": { + "token": $TOKEN + }, + "max_payload": 2MB, + "port": 4222, + ... +} +``` + +### NATS Config Includes + +Any NATS Config key ending in `$include` will be replaced with an include directive. Included files should be in paths relative to `/etc/nats-config`. Multiple `$include` keys are supported by using a prefix, and will be sorted alphabetically. Example: + +```yaml +config: + merge: + 00$include: auth.conf + 01$include: params.conf +configMap: + merge: + data: + auth.conf: | + accounts: { + A: { + users: [ + {user: a, password: a} + ] + }, + B: { + users: [ + {user: b, password: b} + ] + }, + } + params.conf: | + max_payload: 2MB +``` + +templates to the `nats.conf`: + +``` +include auth.conf; +"port": 4222, +... +include params.conf; +``` + +### Extra Resources + +Enables adding additional arbitrary resources. Example - expose WebSocket via VirtualService in Istio: + +```yaml +config: + websocket: + enabled: true +extraResources: +- apiVersion: networking.istio.io/v1beta1 + kind: VirtualService + metadata: + namespace: + $tplYamlSpread: > + {{ include "nats.metadataNamespace" $ }} + name: + $tplYaml: > + {{ include "nats.fullname" $ | quote }} + labels: + $tplYaml: | + {{ include "nats.labels" $ }} + spec: + hosts: + - demo.nats.io + gateways: + - my-gateway + http: + - name: default + match: + - name: root + uri: + exact: / + route: + - destination: + host: + $tplYaml: > + {{ .Values.service.name | quote }} + port: + number: + $tplYaml: > + {{ .Values.config.websocket.port }} +``` diff --git a/infra/charts/nats/UPGRADING.md b/infra/charts/nats/UPGRADING.md new file mode 100644 index 0000000..9cc1779 --- /dev/null +++ b/infra/charts/nats/UPGRADING.md @@ -0,0 +1,155 @@ +# Upgrading from 0.x to 1.x + +Instructions for upgrading an existing `nats` 0.x release to 1.x. + +## Rename Immutable Fields + +There are a number of immutable fields in the NATS Stateful Set and NATS Box deployment. All 1.x `values.yaml` files targeting an existing 0.x release will require some or all of these settings: + +```yaml +config: + # required if using JetStream file storage + jetstream: + # uncomment the next line if using JetStream file storage + # enabled: true + fileStore: + pvc: + name: + $tplYaml: >- + {{ include "nats.fullname" . }}-js-pvc + # set other PVC options here to make it match 0.x, refer to values.yaml for schema + + # required if using a full or cache resolver + resolver: + # uncomment the next line if using a full or cache resolver + # enabled: true + pvc: + name: nats-jwt-pvc + # set other PVC options here to make it match 0.x, refer to values.yaml for schema + +# required +statefulSet: + patch: + - op: remove + path: /spec/selector/matchLabels/app.kubernetes.io~1component + - $tplYamlSpread: |- + {{- if and + .Values.config.jetstream.enabled + .Values.config.jetstream.fileStore.enabled + .Values.config.jetstream.fileStore.pvc.enabled + .Values.config.resolver.enabled + .Values.config.resolver.pvc.enabled + }} + - op: move + from: /spec/volumeClaimTemplates/0 + path: /spec/volumeClaimTemplates/1 + {{- else}} + [] + {{- end }} + +# required +headlessService: + name: + $tplYaml: >- + {{ include "nats.fullname" . }} + +# required unless 0.x values explicitly set nats.serviceAccount.create=false +serviceAccount: + enabled: true + +# required to use new ClusterIP service for Clients accessing NATS +# if using TLS, this may require adding another SAN +service: + # uncomment the next line to disable the new ClusterIP service + # enabled: false + name: + $tplYaml: >- + {{ include "nats.fullname" . }}-svc + +# required if using NatsBox +natsBox: + deployment: + patch: + - op: replace + path: /spec/selector/matchLabels + value: + app: nats-box + - op: add + path: /spec/template/metadata/labels/app + value: nats-box +``` + +## Update NATS Config to new values.yaml schema + +Most values that control the NATS Config have changed and moved under the `config` key. Refer to the 1.x Chart's [values.yaml](values.yaml) for the complete schema. + +After migrating to the new values schema, ensure that changes you expect in the NATS Config files match by templating the old and new config files. + +Template your old 0.x Config Map, this example uses a file called `values-old.yaml`: + +```sh +helm template \ + --version "0.x" \ + -f values-old.yaml \ + -s templates/configmap.yaml \ + nats \ + nats/nats +``` + +Template your new 1.x Config Map, this example uses a file called `values.yaml`: + +```sh +helm template \ + --version "^1-beta" \ + -f values.yaml \ + -s templates/config-map.yaml \ + nats \ + nats/nats +``` + +## Update Kubernetes Resources to new values.yaml schema + +Most values that control Kubernetes Resources have been changed. Refer to the 1.x Chart's [values.yaml](values.yaml) for the complete schema. + +After migrating to the new values schema, ensure that changes you expect in resources match by templating the old and new resources. + +| Resource | 0.x Template File | 1.x Template File | +|-------------------------|---------------------------------|-------------------------------------------| +| Config Map | `templates/configmap.yaml` | `templates/config-map.yaml` | +| Stateful Set | `templates/statefulset.yaml` | `templates/stateful-set.yaml` | +| Headless Service | `templates/service.yaml` | `templates/headless-service.yaml` | +| ClusterIP Service | N/A | `templates/service.yaml` | +| Network Policy | `templates/networkpolicy.yaml` | N/A | +| Pod Disruption Budget | `templates/pdb.yaml` | `templates/pod-disruption-budget.yaml` | +| Service Account | `templates/rbac.yaml` | `templates/service-account.yaml` | +| Resource | `templates/` | `templates/` | +| Resource | `templates/` | `templates/` | +| Prometheus Monitor | `templates/serviceMonitor.yaml` | `templates/pod-monitor.yaml` | +| NatsBox Deployment | `templates/nats-box.yaml` | `templates/nats-box/deployment.yaml` | +| NatsBox Service Account | N/A | `templates/nats-box/service-account.yaml` | +| NatsBox Contents Secret | N/A | `templates/nats-box/contents-secret.yaml` | +| NatsBox Contexts Secret | N/A | `templates/nats-box/contexts-secret.yaml` | + +For example, to check that the Stateful Set matches: + +Template your old 0.x Stateful Set, this example uses a file called `values-old.yaml`: + +```sh +helm template \ + --version "0.x" \ + -f values-old.yaml \ + -s templates/statefulset.yaml \ + nats \ + nats/nats +``` + +Template your new 1.x Stateful Set, this example uses a file called `values.yaml`: + +```sh +helm template \ + --version "^1-beta" \ + -f values.yaml \ + -s templates/stateful-set.yaml \ + nats \ + nats/nats +``` diff --git a/infra/charts/nats/files/config-map.yaml b/infra/charts/nats/files/config-map.yaml new file mode 100644 index 0000000..89ee3c2 --- /dev/null +++ b/infra/charts/nats/files/config-map.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + {{- include "nats.metadataNamespace" $ | nindent 2 }} + name: {{ .Values.configMap.name }} + labels: + {{- include "nats.labels" $ | nindent 4 }} +data: + nats.conf: | + {{- include "nats.formatConfig" .config | nindent 4 }} diff --git a/infra/charts/nats/files/config/cluster.yaml b/infra/charts/nats/files/config/cluster.yaml new file mode 100644 index 0000000..8044525 --- /dev/null +++ b/infra/charts/nats/files/config/cluster.yaml @@ -0,0 +1,32 @@ +{{- with .Values.config.cluster }} +name: {{ $.Values.statefulSet.name }} +port: {{ .port }} +no_advertise: {{ .noAdvertise }} +routes: +{{- $proto := ternary "tls" "nats" .tls.enabled }} +{{- $auth := "" }} +{{- if and .routeURLs.user .routeURLs.password }} + {{- $auth = printf "%s:%s@" (urlquery .routeURLs.user) (urlquery .routeURLs.password) -}} +{{- end }} +{{- $domain := $.Values.headlessService.name }} +{{- if .routeURLs.useFQDN }} + {{- $domain = printf "%s.%s.svc.%s" $domain (include "nats.namespace" $) .routeURLs.k8sClusterDomain }} +{{- end }} +{{- $port := (int .port) }} +{{- range $i, $_ := until (int .replicas) }} +- {{ printf "%s://%s%s-%d.%s:%d" $proto $auth $.Values.statefulSet.name $i $domain $port }} +{{- end }} + +{{- if and .routeURLs.user .routeURLs.password }} +authorization: + user: {{ .routeURLs.user | quote }} + password: {{ .routeURLs.password | quote }} +{{- end }} + +{{- with .tls }} +{{- if .enabled }} +tls: + {{- include "nats.loadMergePatch" (merge (dict "file" "config/tls.yaml" "ctx" (merge (dict "tls" .) $)) .) | nindent 2 }} +{{- end }} +{{- end }} +{{- end }} diff --git a/infra/charts/nats/files/config/config.yaml b/infra/charts/nats/files/config/config.yaml new file mode 100644 index 0000000..92fd96f --- /dev/null +++ b/infra/charts/nats/files/config/config.yaml @@ -0,0 +1,114 @@ +{{- with .Values.config }} + +server_name: << $SERVER_NAME >> +lame_duck_grace_period: 10s +lame_duck_duration: 30s +pid_file: /var/run/nats/nats.pid + +######################################## +# NATS +######################################## +{{- with .nats }} +port: {{ .port }} + +{{- with .tls }} +{{- if .enabled }} +tls: + {{- include "nats.loadMergePatch" (merge (dict "file" "config/tls.yaml" "ctx" (merge (dict "tls" .) $)) .) | nindent 2 }} +{{- end }} +{{- end }} +{{- end }} + +######################################## +# leafnodes +######################################## +{{- with .leafnodes }} +{{- if .enabled }} +leafnodes: + {{- include "nats.loadMergePatch" (merge (dict "file" "config/leafnodes.yaml" "ctx" $) .) | nindent 2 }} +{{- end }} +{{- end }} + +######################################## +# websocket +######################################## +{{- with .websocket }} +{{- if .enabled }} +websocket: + {{- include "nats.loadMergePatch" (merge (dict "file" "config/websocket.yaml" "ctx" $) .) | nindent 2 }} +{{- end }} +{{- end }} + +######################################## +# MQTT +######################################## +{{- with .mqtt }} +{{- if .enabled }} +mqtt: + {{- include "nats.loadMergePatch" (merge (dict "file" "config/mqtt.yaml" "ctx" $) .) | nindent 2 }} +{{- end }} +{{- end }} + +######################################## +# cluster +######################################## +{{- with .cluster }} +{{- if .enabled }} +cluster: + {{- include "nats.loadMergePatch" (merge (dict "file" "config/cluster.yaml" "ctx" $) .) | nindent 2 }} +{{- end }} +{{- end }} + +######################################## +# gateway +######################################## +{{- with .gateway }} +{{- if .enabled }} +gateway: + {{- include "nats.loadMergePatch" (merge (dict "file" "config/gateway.yaml" "ctx" $) .) | nindent 2 }} +{{- end }} +{{- end }} + +######################################## +# monitor +######################################## +{{- with .monitor }} +{{- if .enabled }} +{{- if .tls.enabled }} +https_port: {{ .port }} +{{- else }} +http_port: {{ .port }} +{{- end }} +{{- end }} +{{- end }} + +######################################## +# profiling +######################################## +{{- with .profiling }} +{{- if .enabled }} +prof_port: {{ .port }} +{{- end }} +{{- end }} + +######################################## +# jetstream +######################################## +{{- with $.Values.config.jetstream -}} +{{- if .enabled }} +jetstream: + {{- include "nats.loadMergePatch" (merge (dict "file" "config/jetstream.yaml" "ctx" $) .) | nindent 2 }} +{{- end }} +{{- end }} + +######################################## +# resolver +######################################## +{{- with $.Values.config.resolver -}} +{{- if .enabled }} +resolver: + {{- include "nats.loadMergePatch" (merge (dict "file" "config/resolver.yaml" "ctx" $) .) | nindent 2 }} +{{- end }} +{{- end }} + +{{- end }} diff --git a/infra/charts/nats/files/config/gateway.yaml b/infra/charts/nats/files/config/gateway.yaml new file mode 100644 index 0000000..32d4ed9 --- /dev/null +++ b/infra/charts/nats/files/config/gateway.yaml @@ -0,0 +1,11 @@ +{{- with .Values.config.gateway }} +name: {{ $.Values.statefulSet.name }} +port: {{ .port }} + +{{- with .tls }} +{{- if .enabled }} +tls: + {{- include "nats.loadMergePatch" (merge (dict "file" "config/tls.yaml" "ctx" (merge (dict "tls" .) $)) .) | nindent 2 }} +{{- end }} +{{- end }} +{{- end }} diff --git a/infra/charts/nats/files/config/jetstream.yaml b/infra/charts/nats/files/config/jetstream.yaml new file mode 100644 index 0000000..17262f6 --- /dev/null +++ b/infra/charts/nats/files/config/jetstream.yaml @@ -0,0 +1,23 @@ +{{- with .Values.config.jetstream }} +{{- with .memoryStore }} +{{- if .enabled }} +{{- with .maxSize }} +max_memory_store: << {{ . }} >> +{{- end }} +{{- else }} +max_memory_store: 0 +{{- end }} +{{- end }} +{{- with .fileStore }} +{{- if .enabled }} +store_dir: {{ .dir }} +{{- if .maxSize }} +max_file_store: << {{ .maxSize }} >> +{{- else if .pvc.enabled }} +max_file_store: << {{ .pvc.size }} >> +{{- end }} +{{- else }} +max_file_store: 0 +{{- end }} +{{- end }} +{{- end }} diff --git a/infra/charts/nats/files/config/leafnodes.yaml b/infra/charts/nats/files/config/leafnodes.yaml new file mode 100644 index 0000000..3a1d9a1 --- /dev/null +++ b/infra/charts/nats/files/config/leafnodes.yaml @@ -0,0 +1,11 @@ +{{- with .Values.config.leafnodes }} +port: {{ .port }} +no_advertise: true + +{{- with .tls }} +{{- if .enabled }} +tls: + {{- include "nats.loadMergePatch" (merge (dict "file" "config/tls.yaml" "ctx" (merge (dict "tls" .) $)) .) | nindent 2 }} +{{- end }} +{{- end }} +{{- end }} diff --git a/infra/charts/nats/files/config/mqtt.yaml b/infra/charts/nats/files/config/mqtt.yaml new file mode 100644 index 0000000..e25d8a3 --- /dev/null +++ b/infra/charts/nats/files/config/mqtt.yaml @@ -0,0 +1,10 @@ +{{- with .Values.config.mqtt }} +port: {{ .port }} + +{{- with .tls }} +{{- if .enabled }} +tls: + {{- include "nats.loadMergePatch" (merge (dict "file" "config/tls.yaml" "ctx" (merge (dict "tls" .) $)) .) | nindent 2 }} +{{- end }} +{{- end }} +{{- end }} diff --git a/infra/charts/nats/files/config/protocol.yaml b/infra/charts/nats/files/config/protocol.yaml new file mode 100644 index 0000000..288c80d --- /dev/null +++ b/infra/charts/nats/files/config/protocol.yaml @@ -0,0 +1,10 @@ +{{- with .protocol }} +port: {{ .port }} + +{{- with .tls }} +{{- if .enabled }} +tls: + {{- include "nats.loadMergePatch" (merge (dict "file" "config/tls.yaml" "ctx" (merge (dict "tls" .) $)) .) | nindent 2 }} +{{- end }} +{{- end }} +{{- end }} diff --git a/infra/charts/nats/files/config/resolver.yaml b/infra/charts/nats/files/config/resolver.yaml new file mode 100644 index 0000000..a6761c4 --- /dev/null +++ b/infra/charts/nats/files/config/resolver.yaml @@ -0,0 +1,3 @@ +{{- with .Values.config.resolver }} +dir: {{ .dir }} +{{- end }} diff --git a/infra/charts/nats/files/config/tls.yaml b/infra/charts/nats/files/config/tls.yaml new file mode 100644 index 0000000..26aee01 --- /dev/null +++ b/infra/charts/nats/files/config/tls.yaml @@ -0,0 +1,16 @@ +# tls +{{- with .tls }} +{{- if .secretName }} +{{- $dir := trimSuffix "/" .dir }} +cert_file: {{ printf "%s/%s" $dir (.cert | default "tls.crt") | quote }} +key_file: {{ printf "%s/%s" $dir (.key | default "tls.key") | quote }} +{{- end }} +{{- end }} + +# tlsCA +{{- with $.Values.tlsCA }} +{{- if and .enabled (or .configMapName .secretName) }} +{{- $dir := trimSuffix "/" .dir }} +ca_file: {{ printf "%s/%s" $dir (.key | default "ca.crt") | quote }} +{{- end }} +{{- end }} diff --git a/infra/charts/nats/files/config/websocket.yaml b/infra/charts/nats/files/config/websocket.yaml new file mode 100644 index 0000000..afcd178 --- /dev/null +++ b/infra/charts/nats/files/config/websocket.yaml @@ -0,0 +1,12 @@ +{{- with .Values.config.websocket }} +port: {{ .port }} + +{{- if .tls.enabled }} +{{- with .tls }} +tls: + {{- include "nats.loadMergePatch" (merge (dict "file" "config/tls.yaml" "ctx" (merge (dict "tls" .) $)) .) | nindent 2 }} +{{- end }} +{{- else }} +no_tls: true +{{- end }} +{{- end }} diff --git a/infra/charts/nats/files/headless-service.yaml b/infra/charts/nats/files/headless-service.yaml new file mode 100644 index 0000000..da6552b --- /dev/null +++ b/infra/charts/nats/files/headless-service.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: Service +metadata: + {{- include "nats.metadataNamespace" $ | nindent 2 }} + name: {{ .Values.headlessService.name }} + labels: + {{- include "nats.labels" $ | nindent 4 }} +spec: + selector: + {{- include "nats.selectorLabels" $ | nindent 4 }} + clusterIP: None + publishNotReadyAddresses: true + ports: + {{- range $protocol := list "nats" "leafnodes" "websocket" "mqtt" "cluster" "gateway" "monitor" "profiling" }} + {{- $configProtocol := get $.Values.config $protocol }} + {{- if or (eq $protocol "nats") $configProtocol.enabled }} + {{- $tlsEnabled := false }} + {{- if hasKey $configProtocol "tls" }} + {{- $tlsEnabled = $configProtocol.tls.enabled }} + {{- end }} + {{- $appProtocol := or (eq $protocol "websocket") (eq $protocol "monitor") | ternary ($tlsEnabled | ternary "https" "http") ($tlsEnabled | ternary "tls" "tcp") }} + - {{ dict "name" $protocol "port" $configProtocol.port "targetPort" $protocol "appProtocol" $appProtocol | toYaml | nindent 4 }} + {{- end }} + {{- end }} diff --git a/infra/charts/nats/files/ingress.yaml b/infra/charts/nats/files/ingress.yaml new file mode 100644 index 0000000..b59f0fa --- /dev/null +++ b/infra/charts/nats/files/ingress.yaml @@ -0,0 +1,34 @@ +{{- with .Values.config.websocket.ingress }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + {{- include "nats.metadataNamespace" $ | nindent 2 }} + name: {{ .name }} + labels: + {{- include "nats.labels" $ | nindent 4 }} +spec: + {{- with .className }} + ingressClassName: {{ . | quote }} + {{- end }} + rules: + {{- $path := .path }} + {{- $pathType := .pathType }} + {{- range .hosts }} + - host: {{ . | quote }} + http: + paths: + - path: {{ $path | quote }} + pathType: {{ $pathType | quote }} + backend: + service: + name: {{ $.Values.service.name }} + port: + name: websocket + {{- end }} + {{- if .tlsSecretName }} + tls: + - secretName: {{ .tlsSecretName | quote }} + hosts: + {{- toYaml .hosts | nindent 4 }} + {{- end }} +{{- end }} diff --git a/infra/charts/nats/files/nats-box/contents-secret.yaml b/infra/charts/nats/files/nats-box/contents-secret.yaml new file mode 100644 index 0000000..6e8fdb2 --- /dev/null +++ b/infra/charts/nats/files/nats-box/contents-secret.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Secret +metadata: + {{- include "nats.metadataNamespace" $ | nindent 2 }} + name: {{ .Values.natsBox.contentsSecret.name }} + labels: + {{- include "natsBox.labels" $ | nindent 4 }} +type: Opaque +stringData: + {{- range $ctxKey, $ctxVal := .Values.natsBox.contexts }} + {{- range $secretKey, $secretVal := dict "creds" "creds" "nkey" "nk" }} + {{- $secret := get $ctxVal $secretKey }} + {{- if and $secret $secret.contents }} + "{{ $ctxKey }}.{{ $secretVal }}": {{ $secret.contents | quote }} + {{- end }} + {{- end }} + {{- end }} diff --git a/infra/charts/nats/files/nats-box/contexts-secret/context.yaml b/infra/charts/nats/files/nats-box/contexts-secret/context.yaml new file mode 100644 index 0000000..54480ea --- /dev/null +++ b/infra/charts/nats/files/nats-box/contexts-secret/context.yaml @@ -0,0 +1,51 @@ +{{- $contextName := .contextName }} + +# url +{{- if .Values.service.enabled }} +url: nats://{{ .Values.service.name }} +{{- else }} +url: nats://{{ .Values.headlessService.name }} +{{- end }} + +{{- with .context }} + +# creds +{{- with .creds}} +{{- if .contents }} +creds: /etc/nats-contents/{{ $contextName }}.creds +{{- else if .secretName }} +{{- $dir := trimSuffix "/" .dir }} +creds: {{ printf "%s/%s" $dir (.key | default "nats.creds") | quote }} +{{- end }} +{{- end }} + +# nkey +{{- with .nkey}} +{{- if .contents }} +nkey: /etc/nats-contents/{{ $contextName }}.nk +{{- else if .secretName }} +{{- $dir := trimSuffix "/" .dir }} +nkey: {{ printf "%s/%s" $dir (.key | default "nats.nk") | quote }} +{{- end }} +{{- end }} + +# tls +{{- with .tls }} +{{- if .secretName }} +{{- $dir := trimSuffix "/" .dir }} +cert: {{ printf "%s/%s" $dir (.cert | default "tls.crt") | quote }} +key: {{ printf "%s/%s" $dir (.key | default "tls.key") | quote }} +{{- end }} +{{- end }} + +# tlsCA +{{- if $.Values.config.nats.tls.enabled }} +{{- with $.Values.tlsCA }} +{{- if and .enabled (or .configMapName .secretName) }} +{{- $dir := trimSuffix "/" .dir }} +ca: {{ printf "%s/%s" $dir (.key | default "ca.crt") | quote }} +{{- end }} +{{- end }} +{{- end }} + +{{- end }} diff --git a/infra/charts/nats/files/nats-box/contexts-secret/contexts-secret.yaml b/infra/charts/nats/files/nats-box/contexts-secret/contexts-secret.yaml new file mode 100644 index 0000000..0ce8d1d --- /dev/null +++ b/infra/charts/nats/files/nats-box/contexts-secret/contexts-secret.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Secret +metadata: + {{- include "nats.metadataNamespace" $ | nindent 2 }} + name: {{ .Values.natsBox.contextsSecret.name }} + labels: + {{- include "natsBox.labels" $ | nindent 4 }} +type: Opaque +stringData: +{{- range $ctxKey, $ctxVal := .Values.natsBox.contexts }} + "{{ $ctxKey }}.json": | + {{- include "toPrettyRawJson" (include "nats.loadMergePatch" (dict "file" "nats-box/contexts-secret/context.yaml" "merge" (.merge | default dict) "patch" (.patch | default list) "ctx" (merge (dict "contextName" $ctxKey "context" $ctxVal) $)) | fromYaml) | nindent 4 }} +{{- end }} diff --git a/infra/charts/nats/files/nats-box/deployment/container.yaml b/infra/charts/nats/files/nats-box/deployment/container.yaml new file mode 100644 index 0000000..331c7d6 --- /dev/null +++ b/infra/charts/nats/files/nats-box/deployment/container.yaml @@ -0,0 +1,49 @@ +name: nats-box +{{ include "nats.image" (merge (pick $.Values "global") .Values.natsBox.container.image) }} + +{{- with .Values.natsBox.container.env }} +env: +{{- include "nats.env" . }} +{{- end }} + +command: +- sh +- -ec +- | + work_dir="$(pwd)" + mkdir -p "$XDG_CONFIG_HOME/nats" + cd "$XDG_CONFIG_HOME/nats" + if ! [ -s context ]; then + ln -s /etc/nats-contexts context + fi + {{- if .Values.natsBox.defaultContextName }} + if ! [ -f context.txt ]; then + echo -n {{ .Values.natsBox.defaultContextName | quote }} > context.txt + fi + {{- end }} + cd "$work_dir" + exec /entrypoint.sh "$@" +- -- +args: +- sh +- -ec +- trap true INT TERM; sleep infinity & wait +volumeMounts: +# contexts secret +- name: contexts + mountPath: /etc/nats-contexts +# contents secret +{{- if .hasContentsSecret }} +- name: contents + mountPath: /etc/nats-contents +{{- end }} +# tlsCA +{{- include "nats.tlsCAVolumeMount" $ }} +# secrets +{{- range (include "natsBox.secretNames" $ | fromJson).secretNames }} +- name: {{ .name | quote }} + mountPath: {{ .dir | quote }} +{{- end }} + +resources: + {{- toYaml .Values.natsBox.container.resources | nindent 2 }} \ No newline at end of file diff --git a/infra/charts/nats/files/nats-box/deployment/deployment.yaml b/infra/charts/nats/files/nats-box/deployment/deployment.yaml new file mode 100644 index 0000000..bf39dd8 --- /dev/null +++ b/infra/charts/nats/files/nats-box/deployment/deployment.yaml @@ -0,0 +1,16 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + {{- include "nats.metadataNamespace" $ | nindent 2 }} + name: {{ .Values.natsBox.deployment.name }} + labels: + {{- include "natsBox.labels" $ | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "natsBox.selectorLabels" $ | nindent 6 }} + replicas: 1 + template: + {{- with .Values.natsBox.podTemplate }} + {{ include "nats.loadMergePatch" (merge (dict "file" "nats-box/deployment/pod-template.yaml" "ctx" $) .) | nindent 4 }} + {{- end }} diff --git a/infra/charts/nats/files/nats-box/deployment/pod-template.yaml b/infra/charts/nats/files/nats-box/deployment/pod-template.yaml new file mode 100644 index 0000000..71056bf --- /dev/null +++ b/infra/charts/nats/files/nats-box/deployment/pod-template.yaml @@ -0,0 +1,44 @@ +metadata: + labels: + {{- include "natsBox.labels" $ | nindent 4 }} +spec: + containers: + {{- with .Values.natsBox.container }} + - {{ include "nats.loadMergePatch" (merge (dict "file" "nats-box/deployment/container.yaml" "ctx" $) .) | nindent 4 }} + {{- end }} + + # service discovery uses DNS; don't need service env vars + enableServiceLinks: false + + {{- with .Values.global.image.pullSecretNames }} + imagePullSecrets: + {{- range . }} + - name: {{ . | quote }} + {{- end }} + {{- end }} + + {{- with .Values.natsBox.serviceAccount }} + {{- if .enabled }} + serviceAccountName: {{ .name | quote }} + {{- end }} + {{- end }} + + volumes: + # contexts secret + - name: contexts + secret: + secretName: {{ .Values.natsBox.contextsSecret.name }} + # contents secret + {{- if .hasContentsSecret }} + - name: contents + secret: + secretName: {{ .Values.natsBox.contentsSecret.name }} + {{- end }} + # tlsCA + {{- include "nats.tlsCAVolume" $ | nindent 2 }} + # secrets + {{- range (include "natsBox.secretNames" $ | fromJson).secretNames }} + - name: {{ .name | quote }} + secret: + secretName: {{ .secretName | quote }} + {{- end }} diff --git a/infra/charts/nats/files/nats-box/service-account.yaml b/infra/charts/nats/files/nats-box/service-account.yaml new file mode 100644 index 0000000..c31e52f --- /dev/null +++ b/infra/charts/nats/files/nats-box/service-account.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + {{- include "nats.metadataNamespace" $ | nindent 2 }} + name: {{ .Values.natsBox.serviceAccount.name }} + labels: + {{- include "natsBox.labels" $ | nindent 4 }} diff --git a/infra/charts/nats/files/pod-disruption-budget.yaml b/infra/charts/nats/files/pod-disruption-budget.yaml new file mode 100644 index 0000000..fd1fdea --- /dev/null +++ b/infra/charts/nats/files/pod-disruption-budget.yaml @@ -0,0 +1,12 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + {{- include "nats.metadataNamespace" $ | nindent 2 }} + name: {{ .Values.podDisruptionBudget.name }} + labels: + {{- include "nats.labels" $ | nindent 4 }} +spec: + maxUnavailable: 1 + selector: + matchLabels: + {{- include "nats.selectorLabels" $ | nindent 6 }} diff --git a/infra/charts/nats/files/pod-monitor.yaml b/infra/charts/nats/files/pod-monitor.yaml new file mode 100644 index 0000000..c6c8eae --- /dev/null +++ b/infra/charts/nats/files/pod-monitor.yaml @@ -0,0 +1,13 @@ +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + {{- include "nats.metadataNamespace" $ | nindent 2 }} + name: {{ .Values.promExporter.podMonitor.name }} + labels: + {{- include "nats.labels" $ | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "nats.selectorLabels" $ | nindent 6 }} + podMetricsEndpoints: + - port: prom-metrics diff --git a/infra/charts/nats/files/service-account.yaml b/infra/charts/nats/files/service-account.yaml new file mode 100644 index 0000000..22c18cc --- /dev/null +++ b/infra/charts/nats/files/service-account.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + {{- include "nats.metadataNamespace" $ | nindent 2 }} + name: {{ .Values.serviceAccount.name }} + labels: + {{- include "nats.labels" $ | nindent 4 }} diff --git a/infra/charts/nats/files/service.yaml b/infra/charts/nats/files/service.yaml new file mode 100644 index 0000000..db08fe5 --- /dev/null +++ b/infra/charts/nats/files/service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + {{- include "nats.metadataNamespace" $ | nindent 2 }} + name: {{ .Values.service.name }} + labels: + {{- include "nats.labels" $ | nindent 4 }} +spec: + selector: + {{- include "nats.selectorLabels" $ | nindent 4 }} + ports: + {{- range $protocol := list "nats" "leafnodes" "websocket" "mqtt" "cluster" "gateway" "monitor" "profiling" }} + {{- $configProtocol := get $.Values.config $protocol }} + {{- $servicePort := get $.Values.service.ports $protocol }} + {{- if and (or (eq $protocol "nats") $configProtocol.enabled) $servicePort.enabled }} + {{- $tlsEnabled := false }} + {{- if hasKey $configProtocol "tls" }} + {{- $tlsEnabled = $configProtocol.tls.enabled }} + {{- end }} + {{- $appProtocol := or (eq $protocol "websocket") (eq $protocol "monitor") | ternary ($tlsEnabled | ternary "https" "http") ($tlsEnabled | ternary "tls" "tcp") }} + - {{ merge (dict "name" $protocol "targetPort" $protocol "appProtocol" $appProtocol) (omit $servicePort "enabled") (dict "port" $configProtocol.port) | toYaml | nindent 4 }} + {{- end }} + {{- end }} diff --git a/infra/charts/nats/files/stateful-set/jetstream-pvc.yaml b/infra/charts/nats/files/stateful-set/jetstream-pvc.yaml new file mode 100644 index 0000000..a43f200 --- /dev/null +++ b/infra/charts/nats/files/stateful-set/jetstream-pvc.yaml @@ -0,0 +1,13 @@ +{{- with .Values.config.jetstream.fileStore.pvc }} +metadata: + name: {{ .name }} +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .size | quote }} + {{- with .storageClassName }} + storageClassName: {{ . | quote }} + {{- end }} +{{- end }} diff --git a/infra/charts/nats/files/stateful-set/nats-container.yaml b/infra/charts/nats/files/stateful-set/nats-container.yaml new file mode 100644 index 0000000..8f8646f --- /dev/null +++ b/infra/charts/nats/files/stateful-set/nats-container.yaml @@ -0,0 +1,109 @@ +name: nats +{{ include "nats.image" (merge (pick $.Values "global") .Values.container.image) }} + +ports: +{{- range $protocol := list "nats" "leafnodes" "websocket" "mqtt" "cluster" "gateway" "monitor" "profiling" }} +{{- $configProtocol := get $.Values.config $protocol }} +{{- $containerPort := get $.Values.container.ports $protocol }} +{{- if or (eq $protocol "nats") $configProtocol.enabled }} +- {{ merge (dict "name" $protocol "containerPort" $configProtocol.port) $containerPort | toYaml | nindent 2 }} +{{- end }} +{{- end }} + +args: +- --config +- /etc/nats-config/nats.conf + +env: +- name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name +- name: SERVER_NAME + value: {{ printf "%s$(POD_NAME)" .Values.config.serverNamePrefix | quote }} +{{- with .Values.container.env }} +{{- include "nats.env" . }} +{{- end }} + +lifecycle: + preStop: + exec: + # send the lame duck shutdown signal to trigger a graceful shutdown + command: + - nats-server + - -sl=ldm=/var/run/nats/nats.pid + +{{- with .Values.config.monitor }} +{{- if .enabled }} +startupProbe: + httpGet: + path: /healthz + port: monitor + {{- if .tls.enabled }} + scheme: HTTPS + {{- end}} + initialDelaySeconds: 10 + timeoutSeconds: 5 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 90 +readinessProbe: + httpGet: + path: /healthz?js-server-only=true + port: monitor + {{- if .tls.enabled }} + scheme: HTTPS + {{- end}} + initialDelaySeconds: 10 + timeoutSeconds: 5 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 3 +livenessProbe: + httpGet: + path: /healthz?js-enabled-only=true + port: monitor + {{- if .tls.enabled }} + scheme: HTTPS + {{- end}} + initialDelaySeconds: 10 + timeoutSeconds: 5 + periodSeconds: 30 + successThreshold: 1 + failureThreshold: 3 +{{- end }} +{{- end }} + +volumeMounts: +# nats config +- name: config + mountPath: /etc/nats-config +# PID volume +- name: pid + mountPath: /var/run/nats +# JetStream PVC +{{- with .Values.config.jetstream }} +{{- if and .enabled .fileStore.enabled .fileStore.pvc.enabled }} +{{- with .fileStore }} +- name: {{ .pvc.name }} + mountPath: {{ .dir | quote }} +{{- end }} +{{- end }} +{{- end }} +# resolver PVC +{{- with .Values.config.resolver }} +{{- if and .enabled .pvc.enabled }} +- name: {{ .pvc.name }} + mountPath: {{ .dir | quote }} +{{- end }} +{{- end }} +# tlsCA +{{- include "nats.tlsCAVolumeMount" $ }} +# secrets +{{- range (include "nats.secretNames" $ | fromJson).secretNames }} +- name: {{ .name | quote }} + mountPath: {{ .dir | quote }} +{{- end }} + +resources: + {{- toYaml .Values.container.resources | nindent 2 }} diff --git a/infra/charts/nats/files/stateful-set/pod-template.yaml b/infra/charts/nats/files/stateful-set/pod-template.yaml new file mode 100644 index 0000000..9832ba3 --- /dev/null +++ b/infra/charts/nats/files/stateful-set/pod-template.yaml @@ -0,0 +1,75 @@ +metadata: + labels: + {{- include "nats.labels" $ | nindent 4 }} + annotations: + {{- if .Values.podTemplate.configChecksumAnnotation }} + {{- $configMap := include "nats.loadMergePatch" (merge (dict "file" "config-map.yaml" "ctx" $) $.Values.configMap) }} + checksum/config: {{ sha256sum $configMap }} + {{- end }} +spec: + containers: + # nats + {{- $nats := dict }} + {{- with .Values.container }} + {{- $nats = include "nats.loadMergePatch" (merge (dict "file" "stateful-set/nats-container.yaml" "ctx" $) .) | fromYaml }} + - {{ toYaml $nats | nindent 4 }} + {{- end }} + # reloader + {{- with .Values.reloader }} + {{- if .enabled }} + - {{ include "nats.loadMergePatch" (merge (dict "file" "stateful-set/reloader-container.yaml" "ctx" (merge (dict "natsVolumeMounts" $nats.volumeMounts) $)) .) | nindent 4 }} + {{- end }} + {{- end }} + {{- with .Values.promExporter }} + {{- if .enabled }} + - {{ include "nats.loadMergePatch" (merge (dict "file" "stateful-set/prom-exporter-container.yaml" "ctx" $) .) | nindent 4 }} + {{- end }} + {{- end }} + + # service discovery uses DNS; don't need service env vars + enableServiceLinks: false + + {{- with .Values.global.image.pullSecretNames }} + imagePullSecrets: + {{- range . }} + - name: {{ . | quote }} + {{- end }} + {{- end }} + + {{- with .Values.serviceAccount }} + {{- if .enabled }} + serviceAccountName: {{ .name | quote }} + {{- end }} + {{- end }} + + {{- if .Values.reloader.enabled }} + shareProcessNamespace: true + {{- end }} + + volumes: + # nats config + - name: config + configMap: + name: {{ .Values.configMap.name }} + # PID volume + - name: pid + emptyDir: {} + # tlsCA + {{- include "nats.tlsCAVolume" $ | nindent 2 }} + # secrets + {{- range (include "nats.secretNames" $ | fromJson).secretNames }} + - name: {{ .name | quote }} + secret: + secretName: {{ .secretName | quote }} + {{- end }} + + {{- with .Values.podTemplate.topologySpreadConstraints }} + topologySpreadConstraints: + {{- range $k, $v := . }} + - {{ merge (dict "topologyKey" $k "labelSelector" (dict "matchLabels" (include "nats.selectorLabels" $ | fromYaml))) $v | toYaml | nindent 4 }} + {{- end }} + {{- end}} + + # terminationGracePeriodSeconds determines how long to wait for graceful shutdown + # this should be at least `lameDuckGracePeriod` + 20s shutdown overhead + terminationGracePeriodSeconds: 60 diff --git a/infra/charts/nats/files/stateful-set/prom-exporter-container.yaml b/infra/charts/nats/files/stateful-set/prom-exporter-container.yaml new file mode 100644 index 0000000..75f8a77 --- /dev/null +++ b/infra/charts/nats/files/stateful-set/prom-exporter-container.yaml @@ -0,0 +1,31 @@ +name: prom-exporter +{{ include "nats.image" (merge (pick $.Values "global") .Values.promExporter.image) }} + +ports: +- name: prom-metrics + containerPort: {{ .Values.promExporter.port }} + +{{- with .Values.promExporter.env }} +env: +{{- include "nats.env" . }} +{{- end }} + +args: +- -port={{ .Values.promExporter.port }} +- -connz +- -routez +- -subz +- -varz +- -prefix=nats +- -use_internal_server_id +{{- if .Values.config.jetstream.enabled }} +- -jsz=all +{{- end }} +{{- if .Values.config.leafnodes.enabled }} +- -leafz +{{- end }} +{{- if .Values.config.gateway.enabled }} +- -gatewayz +{{- end }} +{{- $monitorProto := ternary "https" "http" .Values.config.monitor.tls.enabled }} +- {{ $monitorProto }}://{{ .Values.promExporter.monitorDomain }}:{{ .Values.config.monitor.port }}/ diff --git a/infra/charts/nats/files/stateful-set/reloader-container.yaml b/infra/charts/nats/files/stateful-set/reloader-container.yaml new file mode 100644 index 0000000..9672204 --- /dev/null +++ b/infra/charts/nats/files/stateful-set/reloader-container.yaml @@ -0,0 +1,27 @@ +name: reloader +{{ include "nats.image" (merge (pick $.Values "global") .Values.reloader.image) }} + +{{- with .Values.reloader.env }} +env: +{{- include "nats.env" . }} +{{- end }} + +args: +- -pid +- /var/run/nats/nats.pid +- -config +- /etc/nats-config/nats.conf +{{ include "nats.reloaderConfig" (dict "config" .config "dir" "/etc/nats-config") }} + +volumeMounts: +- name: pid + mountPath: /var/run/nats +{{- range $mnt := .natsVolumeMounts }} +{{- $found := false }} +{{- range $.Values.reloader.natsVolumeMountPrefixes }} +{{- if and (not $found) (hasPrefix . $mnt.mountPath) }} +{{- $found = true }} +- {{ toYaml $mnt | nindent 2}} +{{- end }} +{{- end }} +{{- end }} diff --git a/infra/charts/nats/files/stateful-set/resolver-pvc.yaml b/infra/charts/nats/files/stateful-set/resolver-pvc.yaml new file mode 100644 index 0000000..3634cd8 --- /dev/null +++ b/infra/charts/nats/files/stateful-set/resolver-pvc.yaml @@ -0,0 +1,13 @@ +{{- with .Values.config.resolver.pvc }} +metadata: + name: {{ .name }} +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .size | quote }} + {{- with .storageClassName }} + storageClassName: {{ . | quote }} + {{- end }} +{{- end }} diff --git a/infra/charts/nats/files/stateful-set/stateful-set.yaml b/infra/charts/nats/files/stateful-set/stateful-set.yaml new file mode 100644 index 0000000..cd8082c --- /dev/null +++ b/infra/charts/nats/files/stateful-set/stateful-set.yaml @@ -0,0 +1,37 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + {{- include "nats.metadataNamespace" $ | nindent 2 }} + name: {{ .Values.statefulSet.name }} + labels: + {{- include "nats.labels" $ | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "nats.selectorLabels" $ | nindent 6 }} + {{- if .Values.config.cluster.enabled }} + replicas: {{ .Values.config.cluster.replicas }} + {{- else }} + replicas: 1 + {{- end }} + serviceName: {{ .Values.headlessService.name }} + podManagementPolicy: Parallel + template: + {{- with .Values.podTemplate }} + {{ include "nats.loadMergePatch" (merge (dict "file" "stateful-set/pod-template.yaml" "ctx" $) .) | nindent 4 }} + {{- end }} + volumeClaimTemplates: + {{- with .Values.config.jetstream }} + {{- if and .enabled .fileStore.enabled .fileStore.pvc.enabled }} + {{- with .fileStore.pvc }} + - {{ include "nats.loadMergePatch" (merge (dict "file" "stateful-set/jetstream-pvc.yaml" "ctx" $) .) | nindent 4 }} + {{- end }} + {{- end }} + {{- end }} + {{- with .Values.config.resolver }} + {{- if and .enabled .pvc.enabled }} + {{- with .pvc }} + - {{ include "nats.loadMergePatch" (merge (dict "file" "stateful-set/resolver-pvc.yaml" "ctx" $) .) | nindent 4 }} + {{- end }} + {{- end }} + {{- end }} diff --git a/infra/charts/nats/templates/_helpers.tpl b/infra/charts/nats/templates/_helpers.tpl new file mode 100644 index 0000000..d848594 --- /dev/null +++ b/infra/charts/nats/templates/_helpers.tpl @@ -0,0 +1,289 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "nats.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "nats.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "nats.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Print the namespace +*/}} +{{- define "nats.namespace" -}} +{{- default .Release.Namespace .Values.namespaceOverride }} +{{- end }} + +{{/* +Print the namespace for the metadata section +*/}} +{{- define "nats.metadataNamespace" -}} +{{- with .Values.namespaceOverride }} +namespace: {{ . | quote }} +{{- end }} +{{- end }} + +{{/* +Set default values. +*/}} +{{- define "nats.defaultValues" }} +{{- if not .defaultValuesSet }} + {{- $name := include "nats.fullname" . }} + {{- with .Values }} + {{- $_ := set .config.jetstream.fileStore.pvc "name" (.config.jetstream.fileStore.pvc.name | default (printf "%s-js" $name)) }} + {{- $_ := set .config.resolver.pvc "name" (.config.resolver.pvc.name | default (printf "%s-resolver" $name)) }} + {{- $_ := set .config.websocket.ingress "name" (.config.websocket.ingress.name | default (printf "%s-ws" $name)) }} + {{- $_ := set .configMap "name" (.configMap.name | default (printf "%s-config" $name)) }} + {{- $_ := set .headlessService "name" (.headlessService.name | default (printf "%s-headless" $name)) }} + {{- $_ := set .natsBox.contentsSecret "name" (.natsBox.contentsSecret.name | default (printf "%s-box-contents" $name)) }} + {{- $_ := set .natsBox.contextsSecret "name" (.natsBox.contextsSecret.name | default (printf "%s-box-contexts" $name)) }} + {{- $_ := set .natsBox.deployment "name" (.natsBox.deployment.name | default (printf "%s-box" $name)) }} + {{- $_ := set .natsBox.serviceAccount "name" (.natsBox.serviceAccount.name | default (printf "%s-box" $name)) }} + {{- $_ := set .podDisruptionBudget "name" (.podDisruptionBudget.name | default $name) }} + {{- $_ := set .service "name" (.service.name | default $name) }} + {{- $_ := set .serviceAccount "name" (.serviceAccount.name | default $name) }} + {{- $_ := set .statefulSet "name" (.statefulSet.name | default $name) }} + {{- $_ := set .promExporter.podMonitor "name" (.promExporter.podMonitor.name | default $name) }} + {{- end }} + + {{- $values := get (include "tplYaml" (dict "doc" .Values "ctx" $) | fromJson) "doc" }} + {{- $_ := set . "Values" $values }} + + {{- $hasContentsSecret := false }} + {{- range $ctxKey, $ctxVal := .Values.natsBox.contexts }} + {{- range $secretKey, $secretVal := dict "creds" "nats-creds" "nkey" "nats-nkeys" "tls" "nats-certs" }} + {{- $secret := get $ctxVal $secretKey }} + {{- if $secret }} + {{- $_ := set $secret "dir" ($secret.dir | default (printf "/etc/%s/%s" $secretVal $ctxKey)) }} + {{- if and (ne $secretKey "tls") $secret.contents }} + {{- $hasContentsSecret = true }} + {{- end }} + {{- end }} + {{- end }} + {{- end }} + {{- $_ := set $ "hasContentsSecret" $hasContentsSecret }} + + {{- with .Values.config }} + {{- $config := include "nats.loadMergePatch" (merge (dict "file" "config/config.yaml" "ctx" $) .) | fromYaml }} + {{- $_ := set $ "config" $config }} + {{- end }} + + {{- $_ := set . "defaultValuesSet" true }} +{{- end }} +{{- end }} + +{{/* +NATS labels +*/}} +{{- define "nats.labels" -}} +{{- with .Values.global.labels -}} +{{ toYaml . }} +{{ end -}} +helm.sh/chart: {{ include "nats.chart" . }} +{{ include "nats.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +NATS selector labels +*/}} +{{- define "nats.selectorLabels" -}} +app.kubernetes.io/name: {{ include "nats.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/component: nats +{{- end }} + +{{/* +NATS Box labels +*/}} +{{- define "natsBox.labels" -}} +{{- with .Values.global.labels -}} +{{ toYaml . }} +{{ end -}} +helm.sh/chart: {{ include "nats.chart" . }} +{{ include "natsBox.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +NATS Box selector labels +*/}} +{{- define "natsBox.selectorLabels" -}} +app.kubernetes.io/name: {{ include "nats.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/component: nats-box +{{- end }} + +{{/* +Print the image +*/}} +{{- define "nats.image" }} +{{- $image := "" }} +{{- if .digest }} +{{- $image = printf "%s@%s" .repository .digest }} +{{- else }} +{{- $image = printf "%s:%s" .repository .tag }} +{{- end }} +{{- if or .registry .global.image.registry }} +{{- $image = printf "%s/%s" (.registry | default .global.image.registry) $image }} +{{- end }} +{{- if .fullImageName }} +{{- $image = .fullImageName }} +{{- end }} +image: {{ $image }} +{{- if or .pullPolicy .global.image.pullPolicy }} +imagePullPolicy: {{ .pullPolicy | default .global.image.pullPolicy }} +{{- end }} +{{- end }} + +{{- define "nats.secretNames" -}} +{{- $secrets := list }} +{{- range $protocol := list "nats" "leafnodes" "websocket" "mqtt" "cluster" "gateway" }} + {{- $configProtocol := get $.Values.config $protocol }} + {{- if and (or (eq $protocol "nats") $configProtocol.enabled) $configProtocol.tls.enabled $configProtocol.tls.secretName }} + {{- $secrets = append $secrets (merge (dict "name" (printf "%s-tls" $protocol)) $configProtocol.tls) }} + {{- end }} +{{- end }} +{{- toJson (dict "secretNames" $secrets) }} +{{- end }} + +{{- define "natsBox.secretNames" -}} +{{- $secrets := list }} +{{- range $ctxKey, $ctxVal := .Values.natsBox.contexts }} +{{- range $secretKey, $secretVal := dict "creds" "nats-creds" "nkey" "nats-nkeys" "tls" "nats-certs" }} + {{- $secret := get $ctxVal $secretKey }} + {{- if and $secret $secret.secretName }} + {{- $secrets = append $secrets (merge (dict "name" (printf "ctx-%s-%s" $ctxKey $secretKey)) $secret) }} + {{- end }} + {{- end }} +{{- end }} +{{- toJson (dict "secretNames" $secrets) }} +{{- end }} + +{{- define "nats.tlsCAVolume" -}} +{{- with .Values.tlsCA }} +{{- if and .enabled (or .configMapName .secretName) }} +- name: tls-ca +{{- if .configMapName }} + configMap: + name: {{ .configMapName | quote }} +{{- else if .secretName }} + secret: + secretName: {{ .secretName | quote }} +{{- end }} +{{- end }} +{{- end }} +{{- end }} + +{{- define "nats.tlsCAVolumeMount" -}} +{{- with .Values.tlsCA }} +{{- if and .enabled (or .configMapName .secretName) }} +- name: tls-ca + mountPath: {{ .dir | quote }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +translates env var map to list +*/}} +{{- define "nats.env" -}} +{{- range $k, $v := . }} +{{- if kindIs "string" $v }} +- name: {{ $k | quote }} + value: {{ $v | quote }} +{{- else if kindIs "map" $v }} +- {{ merge (dict "name" $k) $v | toYaml | nindent 2 }} +{{- else }} +{{- fail (cat "env var" $k "must be string or map, got" (kindOf $v)) }} +{{- end }} +{{- end }} +{{- end }} + +{{- /* +nats.loadMergePatch +input: map with 4 keys: +- file: name of file to load +- ctx: context to pass to tpl +- merge: interface{} to merge +- patch: []interface{} valid JSON Patch document +output: JSON encoded map with 1 key: +- doc: interface{} patched json result +*/}} +{{- define "nats.loadMergePatch" -}} +{{- $doc := tpl (.ctx.Files.Get (printf "files/%s" .file)) .ctx | fromYaml | default dict -}} +{{- $doc = mergeOverwrite $doc (deepCopy (.merge | default dict)) -}} +{{- get (include "jsonpatch" (dict "doc" $doc "patch" (.patch | default list)) | fromJson ) "doc" | toYaml -}} +{{- end }} + + +{{- /* +nats.reloaderConfig +input: map with 2 keys: +- config: interface{} nats config +- dir: dir config file is in +output: YAML list of reloader config files +*/}} +{{- define "nats.reloaderConfig" -}} + {{- $dir := trimSuffix "/" .dir -}} + {{- with .config -}} + {{- if kindIs "map" . -}} + {{- range $k, $v := . -}} + {{- if or (eq $k "cert_file") (eq $k "key_file") (eq $k "ca_file") }} +- -config +- {{ $v }} + {{- else if hasSuffix "$include" $k }} +- -config +- {{ clean (printf "%s/%s" $dir $v) }} + {{- else }} + {{- include "nats.reloaderConfig" (dict "config" $v "dir" $dir) }} + {{- end -}} + {{- end -}} + {{- end -}} + {{- end -}} +{{- end -}} + + +{{- /* +nats.formatConfig +input: map[string]interface{} +output: string with following format rules +1. keys ending in $natsRaw are unquoted +2. keys ending in $natsInclude are converted to include directives +*/}} +{{- define "nats.formatConfig" -}} + {{- + (regexReplaceAll "\"<<\\s+(.*?)\\s+>>\"" + (regexReplaceAll "\".*\\$include\": \"(.*)\",?" (include "toPrettyRawJson" .) "include ${1};") + "${1}") + -}} +{{- end -}} diff --git a/infra/charts/nats/templates/_jsonpatch.tpl b/infra/charts/nats/templates/_jsonpatch.tpl new file mode 100644 index 0000000..cd42c3b --- /dev/null +++ b/infra/charts/nats/templates/_jsonpatch.tpl @@ -0,0 +1,219 @@ +{{- /* +jsonpatch +input: map with 2 keys: +- doc: interface{} valid JSON document +- patch: []interface{} valid JSON Patch document +output: JSON encoded map with 1 key: +- doc: interface{} patched json result +*/}} +{{- define "jsonpatch" -}} + {{- $params := fromJson (toJson .) -}} + {{- $patches := $params.patch -}} + {{- $docContainer := pick $params "doc" -}} + + {{- range $patch := $patches -}} + {{- if not (hasKey $patch "op") -}} + {{- fail "patch is missing op key" -}} + {{- end -}} + {{- if and (ne $patch.op "add") (ne $patch.op "remove") (ne $patch.op "replace") (ne $patch.op "copy") (ne $patch.op "move") (ne $patch.op "test") -}} + {{- fail (cat "patch has invalid op" $patch.op) -}} + {{- end -}} + {{- if not (hasKey $patch "path") -}} + {{- fail "patch is missing path key" -}} + {{- end -}} + {{- if and (or (eq $patch.op "add") (eq $patch.op "replace") (eq $patch.op "test")) (not (hasKey $patch "value")) -}} + {{- fail (cat "patch with op" $patch.op "is missing value key") -}} + {{- end -}} + {{- if and (or (eq $patch.op "copy") (eq $patch.op "move")) (not (hasKey $patch "from")) -}} + {{- fail (cat "patch with op" $patch.op "is missing from key") -}} + {{- end -}} + + {{- $opPathKeys := list "path" -}} + {{- if or (eq $patch.op "copy") (eq $patch.op "move") -}} + {{- $opPathKeys = append $opPathKeys "from" -}} + {{- end -}} + {{- $reSlice := list -}} + + {{- range $opPathKey := $opPathKeys -}} + {{- $obj := $docContainer -}} + {{- if and (eq $patch.op "copy") (eq $opPathKey "from") -}} + {{- $obj = (fromJson (toJson $docContainer)) -}} + {{- end -}} + {{- $key := "doc" -}} + {{- $lastMap := dict "root" $obj -}} + {{- $lastKey := "root" -}} + {{- $paths := (splitList "/" (get $patch $opPathKey)) -}} + {{- $firstPath := index $paths 0 -}} + {{- if ne (index $paths 0) "" -}} + {{- fail (cat "invalid" $opPathKey (get $patch $opPathKey) "must be empty string or start with /") -}} + {{- end -}} + {{- $paths = slice $paths 1 -}} + + {{- range $path := $paths -}} + {{- $path = replace "~1" "/" $path -}} + {{- $path = replace "~0" "~" $path -}} + + {{- if kindIs "slice" $obj -}} + {{- $mapObj := dict -}} + {{- range $i, $v := $obj -}} + {{- $_ := set $mapObj (toString $i) $v -}} + {{- end -}} + {{- $obj = $mapObj -}} + {{- $_ := set $lastMap $lastKey $obj -}} + {{- $reSlice = prepend $reSlice (dict "lastMap" $lastMap "lastKey" $lastKey "mapObj" $obj) -}} + {{- end -}} + + {{- if kindIs "map" $obj -}} + {{- if not (hasKey $obj $key) -}} + {{- fail (cat "key" $key "does not exist") -}} + {{- end -}} + {{- $lastKey = $key -}} + {{- $lastMap = $obj -}} + {{- $obj = index $obj $key -}} + {{- $key = $path -}} + {{- else -}} + {{- fail (cat "cannot iterate into path" $key "on type" (kindOf $obj)) -}} + {{- end -}} + {{- end -}} + + {{- $_ := set $patch (printf "%sKey" $opPathKey) $key -}} + {{- $_ := set $patch (printf "%sLastKey" $opPathKey) $lastKey -}} + {{- $_ = set $patch (printf "%sLastMap" $opPathKey) $lastMap -}} + {{- end -}} + + {{- if eq $patch.op "move" }} + {{- if and (ne $patch.path $patch.from) (hasPrefix (printf "%s/" $patch.path) (printf "%s/" $patch.from)) -}} + {{- fail (cat "from" $patch.from "may not be a child of path" $patch.path) -}} + {{- end -}} + {{- end -}} + + {{- if or (eq $patch.op "move") (eq $patch.op "copy") (eq $patch.op "test") }} + {{- $key := $patch.fromKey -}} + {{- $lastMap := $patch.fromLastMap -}} + {{- $lastKey := $patch.fromLastKey -}} + {{- $setKey := "value" -}} + {{- if eq $patch.op "test" }} + {{- $key = $patch.pathKey -}} + {{- $lastMap = $patch.pathLastMap -}} + {{- $lastKey = $patch.pathLastKey -}} + {{- $setKey = "testValue" -}} + {{- end -}} + {{- $obj := index $lastMap $lastKey -}} + + {{- if kindIs "map" $obj -}} + {{- if not (hasKey $obj $key) -}} + {{- fail (cat $key "does not exist") -}} + {{- end -}} + {{- $_ := set $patch $setKey (index $obj $key) -}} + + {{- else if kindIs "slice" $obj -}} + {{- $i := atoi $key -}} + {{- if ne $key (toString $i) -}} + {{- fail (cat "cannot convert" $key "to int") -}} + {{- end -}} + {{- if lt $i 0 -}} + {{- fail "slice index <0" -}} + {{- else if lt $i (len $obj) -}} + {{- $_ := set $patch $setKey (index $obj $i) -}} + {{- else -}} + {{- fail "slice index >= slice length" -}} + {{- end -}} + + {{- else -}} + {{- fail (cat "cannot" $patch.op $key "on type" (kindOf $obj)) -}} + {{- end -}} + {{- end -}} + + {{- if or (eq $patch.op "remove") (eq $patch.op "replace") (eq $patch.op "move") }} + {{- $key := $patch.pathKey -}} + {{- $lastMap := $patch.pathLastMap -}} + {{- $lastKey := $patch.pathLastKey -}} + {{- if eq $patch.op "move" }} + {{- $key = $patch.fromKey -}} + {{- $lastMap = $patch.fromLastMap -}} + {{- $lastKey = $patch.fromLastKey -}} + {{- end -}} + {{- $obj := index $lastMap $lastKey -}} + + {{- if kindIs "map" $obj -}} + {{- if not (hasKey $obj $key) -}} + {{- fail (cat $key "does not exist") -}} + {{- end -}} + {{- $_ := unset $obj $key -}} + + {{- else if kindIs "slice" $obj -}} + {{- $i := atoi $key -}} + {{- if ne $key (toString $i) -}} + {{- fail (cat "cannot convert" $key "to int") -}} + {{- end -}} + {{- if lt $i 0 -}} + {{- fail "slice index <0" -}} + {{- else if eq $i 0 -}} + {{- $_ := set $lastMap $lastKey (slice $obj 1) -}} + {{- else if lt $i (sub (len $obj) 1) -}} + {{- $_ := set $lastMap $lastKey (concat (slice $obj 0 $i) (slice $obj (add $i 1) (len $obj))) -}} + {{- else if eq $i (sub (len $obj) 1) -}} + {{- $_ := set $lastMap $lastKey (slice $obj 0 (sub (len $obj) 1)) -}} + {{- else -}} + {{- fail "slice index >= slice length" -}} + {{- end -}} + + {{- else -}} + {{- fail (cat "cannot" $patch.op $key "on type" (kindOf $obj)) -}} + {{- end -}} + {{- end -}} + + {{- if or (eq $patch.op "add") (eq $patch.op "replace") (eq $patch.op "move") (eq $patch.op "copy") }} + {{- $key := $patch.pathKey -}} + {{- $lastMap := $patch.pathLastMap -}} + {{- $lastKey := $patch.pathLastKey -}} + {{- $value := $patch.value -}} + {{- $obj := index $lastMap $lastKey -}} + + {{- if kindIs "map" $obj -}} + {{- $_ := set $obj $key $value -}} + + {{- else if kindIs "slice" $obj -}} + {{- $i := 0 -}} + {{- if eq $key "-" -}} + {{- $i = len $obj -}} + {{- else -}} + {{- $i = atoi $key -}} + {{- if ne $key (toString $i) -}} + {{- fail (cat "cannot convert" $key "to int") -}} + {{- end -}} + {{- end -}} + {{- if lt $i 0 -}} + {{- fail "slice index <0" -}} + {{- else if eq $i 0 -}} + {{- $_ := set $lastMap $lastKey (prepend $obj $value) -}} + {{- else if lt $i (len $obj) -}} + {{- $_ := set $lastMap $lastKey (concat (append (slice $obj 0 $i) $value) (slice $obj $i)) -}} + {{- else if eq $i (len $obj) -}} + {{- $_ := set $lastMap $lastKey (append $obj $value) -}} + {{- else -}} + {{- fail "slice index > slice length" -}} + {{- end -}} + + {{- else -}} + {{- fail (cat "cannot" $patch.op $key "on type" (kindOf $obj)) -}} + {{- end -}} + {{- end -}} + + {{- if eq $patch.op "test" }} + {{- if not (deepEqual $patch.value $patch.testValue) }} + {{- fail (cat "test failed, expected" (toJson $patch.value) "but got" (toJson $patch.testValue)) -}} + {{- end -}} + {{- end -}} + + {{- range $reSliceOp := $reSlice -}} + {{- $sliceObj := list -}} + {{- range $i := until (len $reSliceOp.mapObj) -}} + {{- $sliceObj = append $sliceObj (index $reSliceOp.mapObj (toString $i)) -}} + {{- end -}} + {{- $_ := set $reSliceOp.lastMap $reSliceOp.lastKey $sliceObj -}} + {{- end -}} + + {{- end -}} + {{- toJson $docContainer -}} +{{- end -}} diff --git a/infra/charts/nats/templates/_toPrettyRawJson.tpl b/infra/charts/nats/templates/_toPrettyRawJson.tpl new file mode 100644 index 0000000..612a62f --- /dev/null +++ b/infra/charts/nats/templates/_toPrettyRawJson.tpl @@ -0,0 +1,28 @@ +{{- /* +toPrettyRawJson +input: interface{} valid JSON document +output: pretty raw JSON string +*/}} +{{- define "toPrettyRawJson" -}} + {{- include "toPrettyRawJsonStr" (toPrettyJson .) -}} +{{- end -}} + +{{- /* +toPrettyRawJsonStr +input: pretty JSON string +output: pretty raw JSON string +*/}} +{{- define "toPrettyRawJsonStr" -}} + {{- $s := + (regexReplaceAll "([^\\\\](?:\\\\\\\\)*)\\\\u003e" + (regexReplaceAll "([^\\\\](?:\\\\\\\\)*)\\\\u003c" + (regexReplaceAll "([^\\\\](?:\\\\\\\\)*)\\\\u0026" . "${1}&") + "${1}<") + "${1}>") + -}} + {{- if regexMatch "([^\\\\](?:\\\\\\\\)*)\\\\u00(26|3c|3e)" $s -}} + {{- include "toPrettyRawJsonStr" $s -}} + {{- else -}} + {{- $s -}} + {{- end -}} +{{- end -}} diff --git a/infra/charts/nats/templates/_tplYaml.tpl b/infra/charts/nats/templates/_tplYaml.tpl new file mode 100644 index 0000000..f42b9c1 --- /dev/null +++ b/infra/charts/nats/templates/_tplYaml.tpl @@ -0,0 +1,114 @@ +{{- /* +tplYaml +input: map with 2 keys: +- doc: interface{} +- ctx: context to pass to tpl function +output: JSON encoded map with 1 key: +- doc: interface{} with any keys called tpl or tplSpread values templated and replaced + +maps matching the following syntax will be templated and parsed as YAML +{ + $tplYaml: string +} + +maps matching the follow syntax will be templated, parsed as YAML, and spread into the parent map/slice +{ + $tplYamlSpread: string +} +*/}} +{{- define "tplYaml" -}} + {{- $patch := get (include "tplYamlItr" (dict "ctx" .ctx "parentKind" "" "parentPath" "" "path" "/" "value" .doc) | fromJson) "patch" -}} + {{- include "jsonpatch" (dict "doc" .doc "patch" $patch) -}} +{{- end -}} + +{{- /* +tplYamlItr +input: map with 4 keys: +- path: string JSONPath to current element +- parentKind: string kind of parent element +- parentPath: string JSONPath to parent element +- value: interface{} +- ctx: context to pass to tpl function +output: JSON encoded map with 1 key: +- patch: list of patches to apply in order to template +*/}} +{{- define "tplYamlItr" -}} + {{- $params := . -}} + {{- $kind := kindOf $params.value -}} + {{- $patch := list -}} + {{- $joinPath := $params.path -}} + {{- if eq $params.path "/" -}} + {{- $joinPath = "" -}} + {{- end -}} + {{- $joinParentPath := $params.parentPath -}} + {{- if eq $params.parentPath "/" -}} + {{- $joinParentPath = "" -}} + {{- end -}} + + {{- if eq $kind "slice" -}} + {{- $iAdj := 0 -}} + {{- range $i, $v := $params.value -}} + {{- $iPath := printf "%s/%d" $joinPath (add $i $iAdj) -}} + {{- $itrPatch := get (include "tplYamlItr" (dict "ctx" $params.ctx "parentKind" $kind "parentPath" $params.path "path" $iPath "value" $v) | fromJson) "patch" -}} + {{- $itrLen := len $itrPatch -}} + {{- if gt $itrLen 0 -}} + {{- $patch = concat $patch $itrPatch -}} + {{- if eq (get (index $itrPatch 0) "op") "remove" -}} + {{- $iAdj = add $iAdj (sub $itrLen 2) -}} + {{- end -}} + {{- end -}} + {{- end -}} + + {{- else if eq $kind "map" -}} + {{- if and (eq (len $params.value) 1) (or (hasKey $params.value "$tplYaml") (hasKey $params.value "$tplYamlSpread")) -}} + {{- $tpl := get $params.value "$tplYaml" -}} + {{- $spread := false -}} + {{- if hasKey $params.value "$tplYamlSpread" -}} + {{- if eq $params.path "/" -}} + {{- fail "cannot $tplYamlSpread on root object" -}} + {{- end -}} + {{- $tpl = get $params.value "$tplYamlSpread" -}} + {{- $spread = true -}} + {{- end -}} + + {{- $res := tpl $tpl $params.ctx -}} + {{- $res = get (fromYaml (tpl "tpl: {{ nindent 2 .res }}" (merge (dict "res" $res) $params.ctx))) "tpl" -}} + + {{- if eq $spread false -}} + {{- $patch = append $patch (dict "op" "replace" "path" $params.path "value" $res) -}} + {{- else -}} + {{- $resKind := kindOf $res -}} + {{- if and (ne $resKind "invalid") (ne $resKind $params.parentKind) -}} + {{- fail (cat "can only $tplYamlSpread slice onto a slice or map onto a map; attempted to spread" $resKind "on" $params.parentKind "at path" $params.path) -}} + {{- end -}} + {{- $patch = append $patch (dict "op" "remove" "path" $params.path) -}} + {{- if eq $resKind "invalid" -}} + {{- /* no-op */ -}} + {{- else if eq $resKind "slice" -}} + {{- range $v := reverse $res -}} + {{- $patch = append $patch (dict "op" "add" "path" $params.path "value" $v) -}} + {{- end -}} + {{- else -}} + {{- range $k, $v := $res -}} + {{- $kPath := replace "~" "~0" $k -}} + {{- $kPath = replace "/" "~1" $kPath -}} + {{- $kPath = printf "%s/%s" $joinParentPath $kPath -}} + {{- $patch = append $patch (dict "op" "add" "path" $kPath "value" $v) -}} + {{- end -}} + {{- end -}} + {{- end -}} + {{- else -}} + {{- range $k, $v := $params.value -}} + {{- $kPath := replace "~" "~0" $k -}} + {{- $kPath = replace "/" "~1" $kPath -}} + {{- $kPath = printf "%s/%s" $joinPath $kPath -}} + {{- $itrPatch := get (include "tplYamlItr" (dict "ctx" $params.ctx "parentKind" $kind "parentPath" $params.path "path" $kPath "value" $v) | fromJson) "patch" -}} + {{- if gt (len $itrPatch) 0 -}} + {{- $patch = concat $patch $itrPatch -}} + {{- end -}} + {{- end -}} + {{- end -}} + {{- end -}} + + {{- toJson (dict "patch" $patch) -}} +{{- end -}} diff --git a/infra/charts/nats/templates/config-map.yaml b/infra/charts/nats/templates/config-map.yaml new file mode 100644 index 0000000..b95afda --- /dev/null +++ b/infra/charts/nats/templates/config-map.yaml @@ -0,0 +1,4 @@ +{{- include "nats.defaultValues" . }} +{{- with .Values.configMap }} +{{- include "nats.loadMergePatch" (merge (dict "file" "config-map.yaml" "ctx" $) .) }} +{{- end }} diff --git a/infra/charts/nats/templates/extra-resources.yaml b/infra/charts/nats/templates/extra-resources.yaml new file mode 100644 index 0000000..c11f008 --- /dev/null +++ b/infra/charts/nats/templates/extra-resources.yaml @@ -0,0 +1,5 @@ +{{- include "nats.defaultValues" . }} +{{- range .Values.extraResources }} +--- +{{ . | toYaml }} +{{- end }} diff --git a/infra/charts/nats/templates/headless-service.yaml b/infra/charts/nats/templates/headless-service.yaml new file mode 100644 index 0000000..f11a83d --- /dev/null +++ b/infra/charts/nats/templates/headless-service.yaml @@ -0,0 +1,4 @@ +{{- include "nats.defaultValues" . }} +{{- with .Values.headlessService }} +{{- include "nats.loadMergePatch" (merge (dict "file" "headless-service.yaml" "ctx" $) .) }} +{{- end }} diff --git a/infra/charts/nats/templates/ingress.yaml b/infra/charts/nats/templates/ingress.yaml new file mode 100644 index 0000000..eccd73f --- /dev/null +++ b/infra/charts/nats/templates/ingress.yaml @@ -0,0 +1,6 @@ +{{- include "nats.defaultValues" . }} +{{- with .Values.config.websocket.ingress }} +{{- if and .enabled .hosts $.Values.config.websocket.enabled $.Values.service.enabled $.Values.service.ports.websocket.enabled }} +{{- include "nats.loadMergePatch" (merge (dict "file" "ingress.yaml" "ctx" $) .) }} +{{- end }} +{{- end }} diff --git a/infra/charts/nats/templates/nats-box/contents-secret.yaml b/infra/charts/nats/templates/nats-box/contents-secret.yaml new file mode 100644 index 0000000..db629bf --- /dev/null +++ b/infra/charts/nats/templates/nats-box/contents-secret.yaml @@ -0,0 +1,10 @@ +{{- include "nats.defaultValues" . }} +{{- if .hasContentsSecret }} +{{- with .Values.natsBox }} +{{- if .enabled }} +{{- with .contentsSecret}} +{{- include "nats.loadMergePatch" (merge (dict "file" "nats-box/contents-secret.yaml" "ctx" $) .) }} +{{- end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/infra/charts/nats/templates/nats-box/contexts-secret.yaml b/infra/charts/nats/templates/nats-box/contexts-secret.yaml new file mode 100644 index 0000000..5ae20f4 --- /dev/null +++ b/infra/charts/nats/templates/nats-box/contexts-secret.yaml @@ -0,0 +1,8 @@ +{{- include "nats.defaultValues" . }} +{{- with .Values.natsBox }} +{{- if .enabled }} +{{- with .contextsSecret}} +{{- include "nats.loadMergePatch" (merge (dict "file" "nats-box/contexts-secret/contexts-secret.yaml" "ctx" $) .) }} +{{- end }} +{{- end }} +{{- end }} diff --git a/infra/charts/nats/templates/nats-box/deployment.yaml b/infra/charts/nats/templates/nats-box/deployment.yaml new file mode 100644 index 0000000..a063332 --- /dev/null +++ b/infra/charts/nats/templates/nats-box/deployment.yaml @@ -0,0 +1,8 @@ +{{- include "nats.defaultValues" . }} +{{- with .Values.natsBox }} +{{- if .enabled }} +{{- with .deployment }} +{{- include "nats.loadMergePatch" (merge (dict "file" "nats-box/deployment/deployment.yaml" "ctx" $) .) }} +{{- end }} +{{- end }} +{{- end }} diff --git a/infra/charts/nats/templates/nats-box/service-account.yaml b/infra/charts/nats/templates/nats-box/service-account.yaml new file mode 100644 index 0000000..e11bdd3 --- /dev/null +++ b/infra/charts/nats/templates/nats-box/service-account.yaml @@ -0,0 +1,8 @@ +{{- include "nats.defaultValues" . }} +{{- if .Values.natsBox.enabled }} +{{- with .Values.natsBox.serviceAccount }} +{{- if .enabled }} +{{- include "nats.loadMergePatch" (merge (dict "file" "nats-box/service-account.yaml" "ctx" $) .) }} +{{- end }} +{{- end }} +{{- end }} diff --git a/infra/charts/nats/templates/pod-disruption-budget.yaml b/infra/charts/nats/templates/pod-disruption-budget.yaml new file mode 100644 index 0000000..9117226 --- /dev/null +++ b/infra/charts/nats/templates/pod-disruption-budget.yaml @@ -0,0 +1,6 @@ +{{- include "nats.defaultValues" . }} +{{- with .Values.podDisruptionBudget }} +{{- if .enabled }} +{{- include "nats.loadMergePatch" (merge (dict "file" "pod-disruption-budget.yaml" "ctx" $) .) }} +{{- end }} +{{- end }} diff --git a/infra/charts/nats/templates/pod-monitor.yaml b/infra/charts/nats/templates/pod-monitor.yaml new file mode 100644 index 0000000..0e42a43 --- /dev/null +++ b/infra/charts/nats/templates/pod-monitor.yaml @@ -0,0 +1,8 @@ +{{- include "nats.defaultValues" . }} +{{- with .Values.promExporter }} +{{- if and .enabled .podMonitor.enabled }} +{{- with .podMonitor }} +{{- include "nats.loadMergePatch" (merge (dict "file" "pod-monitor.yaml" "ctx" $) .) }} +{{- end }} +{{- end }} +{{- end }} diff --git a/infra/charts/nats/templates/service-account.yaml b/infra/charts/nats/templates/service-account.yaml new file mode 100644 index 0000000..6c763bd --- /dev/null +++ b/infra/charts/nats/templates/service-account.yaml @@ -0,0 +1,6 @@ +{{- include "nats.defaultValues" . }} +{{- with .Values.serviceAccount }} +{{- if .enabled }} +{{- include "nats.loadMergePatch" (merge (dict "file" "service-account.yaml" "ctx" $) .) }} +{{- end }} +{{- end }} diff --git a/infra/charts/nats/templates/service.yaml b/infra/charts/nats/templates/service.yaml new file mode 100644 index 0000000..04b0b37 --- /dev/null +++ b/infra/charts/nats/templates/service.yaml @@ -0,0 +1,6 @@ +{{- include "nats.defaultValues" . }} +{{- with .Values.service }} +{{- if .enabled }} +{{- include "nats.loadMergePatch" (merge (dict "file" "service.yaml" "ctx" $) .) }} +{{- end }} +{{- end }} diff --git a/infra/charts/nats/templates/stateful-set.yaml b/infra/charts/nats/templates/stateful-set.yaml new file mode 100644 index 0000000..bb19832 --- /dev/null +++ b/infra/charts/nats/templates/stateful-set.yaml @@ -0,0 +1,4 @@ +{{- include "nats.defaultValues" . }} +{{- with .Values.statefulSet }} +{{- include "nats.loadMergePatch" (merge (dict "file" "stateful-set/stateful-set.yaml" "ctx" $) .) }} +{{- end }} diff --git a/infra/charts/nats/templates/tests/request-reply.yaml b/infra/charts/nats/templates/tests/request-reply.yaml new file mode 100644 index 0000000..3e06edc --- /dev/null +++ b/infra/charts/nats/templates/tests/request-reply.yaml @@ -0,0 +1,37 @@ +{{- include "nats.defaultValues" . }} +{{- with .Values.natsBox | deepCopy }} +{{- $natsBox := . }} +{{- if .enabled -}} +apiVersion: v1 +kind: Pod +{{- with .container }} +{{- $_ := set . "merge" (dict + "args" (list + "sh" + "-ec" + "nats reply --echo echo & pid=\"$!\"; sleep 1; nats request echo hi > /tmp/resp; kill \"$pid\"; wait; grep -qF hi /tmp/resp" + ) +) }} +{{- $_ := set . "patch" list }} +{{- end }} +{{- with .podTemplate }} +{{- $_ := set . "merge" (dict + "metadata" (dict + "name" (printf "%s-test-request-reply" $.Values.statefulSet.name) + "labels" (dict + "app.kubernetes.io/component" "test-request-reply" + ) + "annotations" (dict + "helm.sh/hook" "test" + "helm.sh/hook-delete-policy" "before-hook-creation,hook-succeeded" + ) + ) + "spec" (dict + "restartPolicy" "Never" + ) +) }} +{{- $_ := set . "patch" list }} +{{ include "nats.loadMergePatch" (merge (dict "file" "nats-box/deployment/pod-template.yaml" "ctx" (merge (dict "Values" (dict "natsBox" $natsBox)) $)) .) }} +{{- end }} +{{- end }} +{{- end }} diff --git a/infra/charts/nats/values.yaml b/infra/charts/nats/values.yaml new file mode 100644 index 0000000..5c025aa --- /dev/null +++ b/infra/charts/nats/values.yaml @@ -0,0 +1,696 @@ +################################################################################ +# Global options +################################################################################ +global: + image: + # global image pull policy to use for all container images in the chart + # can be overridden by individual image pullPolicy + pullPolicy: + # global list of secret names to use as image pull secrets for all pod specs in the chart + # secrets must exist in the same namespace + # https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + pullSecretNames: [] + # global registry to use for all container images in the chart + # can be overridden by individual image registry + registry: + + # global labels will be applied to all resources deployed by the chart + labels: {} + +################################################################################ +# Common options +################################################################################ +# override name of the chart +nameOverride: +# override full name of the chart+release +fullnameOverride: +# override the namespace that resources are installed into +namespaceOverride: + +# reference a common CA Certificate or Bundle in all nats config `tls` blocks and nats-box contexts +# note: `tls.verify` still must be set in the appropriate nats config `tls` blocks to require mTLS +tlsCA: + enabled: false + # set configMapName in order to mount an existing configMap to dir + configMapName: + # set secretName in order to mount an existing secretName to dir + secretName: + # directory to mount the configMap or secret to + dir: /etc/nats-ca-cert + # key in the configMap or secret that contains the CA Certificate or Bundle + key: ca.crt + +################################################################################ +# NATS Stateful Set and associated resources +################################################################################ + +############################################################ +# NATS config +############################################################ +config: + cluster: + enabled: false + port: 6222 + # must be 2 or higher when jetstream is enabled + replicas: 3 + # set to false to allow cluster nodes to advertise their addresses + # so that clients can reconnect without extra DNS lookups. + # Note: in case clients have external connectivity make sure to define the `advertise` section as well. + # If clients are behind a load balancer it is best to leave this as is. + noAdvertise: true + + # apply to generated route URLs that connect to other pods in the StatefulSet + routeURLs: + # if both user and password are set, they will be added to route URLs + # and the cluster authorization block + user: + password: + # set to true to use FQDN in route URLs + useFQDN: false + k8sClusterDomain: cluster.local + + tls: + enabled: false + # set secretName in order to mount an existing secret to dir + secretName: + dir: /etc/nats-certs/cluster + cert: tls.crt + key: tls.key + # merge or patch the tls config + # https://docs.nats.io/running-a-nats-service/configuration/securing_nats/tls + merge: {} + patch: [] + + # merge or patch the cluster config + # https://docs.nats.io/running-a-nats-service/configuration/clustering/cluster_config + merge: {} + patch: [] + + jetstream: + enabled: false + + fileStore: + enabled: true + dir: /data + + ############################################################ + # stateful set -> volume claim templates -> jetstream pvc + ############################################################ + pvc: + enabled: true + size: 10Gi + storageClassName: + + # merge or patch the jetstream pvc + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#persistentvolumeclaim-v1-core + merge: {} + patch: [] + # defaults to "{{ include "nats.fullname" $ }}-js" + name: + + # defaults to the PVC size + maxSize: + + memoryStore: + enabled: false + # ensure that container has a sufficient memory limit greater than maxSize + maxSize: 1Gi + + # merge or patch the jetstream config + # https://docs.nats.io/running-a-nats-service/configuration#jetstream + merge: {} + patch: [] + + nats: + port: 4222 + tls: + enabled: false + # set secretName in order to mount an existing secret to dir + secretName: + dir: /etc/nats-certs/nats + cert: tls.crt + key: tls.key + # merge or patch the tls config + # https://docs.nats.io/running-a-nats-service/configuration/securing_nats/tls + merge: {} + patch: [] + + leafnodes: + enabled: false + port: 7422 + tls: + enabled: false + # set secretName in order to mount an existing secret to dir + secretName: + dir: /etc/nats-certs/leafnodes + cert: tls.crt + key: tls.key + # merge or patch the tls config + # https://docs.nats.io/running-a-nats-service/configuration/securing_nats/tls + merge: {} + patch: [] + + # merge or patch the leafnodes config + # https://docs.nats.io/running-a-nats-service/configuration/leafnodes/leafnode_conf + merge: {} + patch: [] + + websocket: + enabled: false + port: 8080 + tls: + enabled: false + # set secretName in order to mount an existing secret to dir + secretName: + dir: /etc/nats-certs/websocket + cert: tls.crt + key: tls.key + # merge or patch the tls config + # https://docs.nats.io/running-a-nats-service/configuration/securing_nats/tls + merge: {} + patch: [] + + ############################################################ + # ingress + ############################################################ + # service must be enabled also + ingress: + enabled: false + # must contain at least 1 host otherwise ingress will not be created + hosts: [] + path: / + pathType: Exact + # sets to the ingress class name + className: + # set to an existing secret name to enable TLS on the ingress; applies to all hosts + tlsSecretName: + + # merge or patch the ingress + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#ingress-v1-networking-k8s-io + merge: {} + patch: [] + # defaults to "{{ include "nats.fullname" $ }}-ws" + name: + + # merge or patch the websocket config + # https://docs.nats.io/running-a-nats-service/configuration/websocket/websocket_conf + merge: {} + patch: [] + + mqtt: + enabled: false + port: 1883 + tls: + enabled: false + # set secretName in order to mount an existing secret to dir + secretName: + dir: /etc/nats-certs/mqtt + cert: tls.crt + key: tls.key + # merge or patch the tls config + # https://docs.nats.io/running-a-nats-service/configuration/securing_nats/tls + merge: {} + patch: [] + + # merge or patch the mqtt config + # https://docs.nats.io/running-a-nats-service/configuration/mqtt/mqtt_config + merge: {} + patch: [] + + gateway: + enabled: false + port: 7222 + tls: + enabled: false + # set secretName in order to mount an existing secret to dir + secretName: + dir: /etc/nats-certs/gateway + cert: tls.crt + key: tls.key + # merge or patch the tls config + # https://docs.nats.io/running-a-nats-service/configuration/securing_nats/tls + merge: {} + patch: [] + + # merge or patch the gateway config + # https://docs.nats.io/running-a-nats-service/configuration/gateways/gateway#gateway-configuration-block + merge: {} + patch: [] + + monitor: + enabled: true + port: 8222 + tls: + # config.nats.tls must be enabled also + # when enabled, monitoring port will use HTTPS with the options from config.nats.tls + # if promExporter is also enabled, consider setting promExporter.monitorDomain + enabled: false + + profiling: + enabled: false + port: 65432 + + resolver: + enabled: false + dir: /data/resolver + + ############################################################ + # stateful set -> volume claim templates -> resolver pvc + ############################################################ + pvc: + enabled: true + size: 1Gi + storageClassName: + + # merge or patch the pvc + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#persistentvolumeclaim-v1-core + merge: {} + patch: [] + # defaults to "{{ include "nats.fullname" $ }}-resolver" + name: + + # merge or patch the resolver + # https://docs.nats.io/running-a-nats-service/configuration/securing_nats/auth_intro/jwt/resolver + merge: {} + patch: [] + + # adds a prefix to the server name, which defaults to the pod name + # helpful for ensuring server name is unique in a super cluster + serverNamePrefix: "" + + # merge or patch the nats config + # https://docs.nats.io/running-a-nats-service/configuration + # following special rules apply + # 1. strings that start with << and end with >> will be unquoted + # use this for variables and numbers with units + # 2. keys ending in $include will be switched to include directives + # keys are sorted alphabetically, use prefix before $includes to control includes ordering + # paths should be relative to /etc/nats-config/nats.conf + # example: + # + # merge: + # $include: ./my-config.conf + # zzz$include: ./my-config-last.conf + # server_name: nats + # authorization: + # token: << $TOKEN >> + # jetstream: + # max_memory_store: << 1GB >> + # + # will yield the config: + # { + # include ./my-config.conf; + # "authorization": { + # "token": $TOKEN + # }, + # "jetstream": { + # "max_memory_store": 1GB + # }, + # "server_name": "nats", + # include ./my-config-last.conf; + # } + merge: {} + patch: [] + +############################################################ +# stateful set -> pod template -> nats container +############################################################ +container: + image: + repository: nats + tag: 2.12.5-alpine + pullPolicy: + registry: + # if digest is provided, it overrides tag (example: "sha256:abcdef1234567890") + digest: + # if fullImageName is provided, it overrides registry, repository, tag, and digest + fullImageName: + + # container port options + # must be enabled in the config section also + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#containerport-v1-core + ports: + nats: {} + leafnodes: {} + websocket: {} + mqtt: {} + cluster: {} + gateway: {} + monitor: {} + profiling: {} + + # map with key as env var name, value can be string or map + # example: + # + # env: + # GOMEMLIMIT: 7GiB + # TOKEN: + # valueFrom: + # secretKeyRef: + # name: nats-auth + # key: token + env: {} + + # merge or patch the container + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#container-v1-core + merge: {} + patch: [] + + # container resources + resources: {} + # requests: + # cpu: 100m + # memory: 128Mi + # limits: + # cpu: 100m + # memory: 128Mi + +############################################################ +# stateful set -> pod template -> reloader container +############################################################ +reloader: + enabled: true + image: + repository: natsio/nats-server-config-reloader + tag: 0.21.1 + pullPolicy: + registry: + digest: + fullImageName: + + # env var map, see nats.env for an example + env: {} + + # all nats container volume mounts with the following prefixes + # will be mounted into the reloader container + natsVolumeMountPrefixes: + - /etc/ + + # merge or patch the container + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#container-v1-core + merge: {} + patch: [] + +############################################################ +# stateful set -> pod template -> prom-exporter container +############################################################ +# config.monitor must be enabled +promExporter: + enabled: false + image: + repository: natsio/prometheus-nats-exporter + tag: 0.18.0 + pullPolicy: + registry: + digest: + fullImageName: + + port: 7777 + # if config.monitor.tls.enabled is set to true, monitorDomain must be set to the common name + # or a SAN used in the tls certificate + monitorDomain: localhost + # env var map, see nats.env for an example + env: {} + + # merge or patch the container + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#container-v1-core + merge: {} + patch: [] + + ############################################################ + # prometheus pod monitor + ############################################################ + podMonitor: + enabled: false + + # merge or patch the pod monitor + # https://prometheus-operator.dev/docs/api-reference/api/#monitoring.coreos.com/v1.PodMonitor + merge: {} + patch: [] + # defaults to "{{ include "nats.fullname" $ }}" + name: + +############################################################ +# service +############################################################ +service: + enabled: true + + # service port options + # additional boolean field enable to control whether port is exposed in the service + # must be enabled in the config section also + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#serviceport-v1-core + ports: + nats: + enabled: true + leafnodes: + enabled: true + websocket: + enabled: true + mqtt: + enabled: true + cluster: + enabled: false + gateway: + enabled: false + monitor: + enabled: false + profiling: + enabled: false + + # merge or patch the service + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#service-v1-core + merge: {} + patch: [] + # defaults to "{{ include "nats.fullname" $ }}" + name: + +############################################################ +# other nats extension points +############################################################ + +# stateful set +statefulSet: + # merge or patch the stateful set + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#statefulset-v1-apps + merge: {} + patch: [] + # defaults to "{{ include "nats.fullname" $ }}" + name: + +# stateful set -> pod template +podTemplate: + # adds a hash of the ConfigMap as a pod annotation + # this will cause the StatefulSet to roll when the ConfigMap is updated + # set to true to force pod rollouts on config changes instead of using the reloader for hot updates + configChecksumAnnotation: false + + # map of topologyKey: topologySpreadConstraint + # labelSelector will be added to match StatefulSet pods + # + # topologySpreadConstraints: + # kubernetes.io/hostname: + # maxSkew: 1 + # + topologySpreadConstraints: {} + + # merge or patch the pod template + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#pod-v1-core + merge: {} + patch: [] + +# headless service +headlessService: + # merge or patch the headless service + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#service-v1-core + merge: {} + patch: [] + # defaults to "{{ include "nats.fullname" $ }}-headless" + name: + +# config map +configMap: + # merge or patch the config map + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#configmap-v1-core + merge: {} + patch: [] + # defaults to "{{ include "nats.fullname" $ }}-config" + name: + +# pod disruption budget +podDisruptionBudget: + enabled: true + # merge or patch the pod disruption budget + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#poddisruptionbudget-v1-policy + merge: {} + patch: [] + # defaults to "{{ include "nats.fullname" $ }}" + name: + +# service account +serviceAccount: + enabled: false + # merge or patch the service account + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#serviceaccount-v1-core + merge: {} + patch: [] + # defaults to "{{ include "nats.fullname" $ }}" + name: + +############################################################ +# natsBox +# +# NATS Box Deployment and associated resources +############################################################ +natsBox: + enabled: true + + ############################################################ + # NATS contexts + ############################################################ + contexts: + default: + creds: + # set contents in order to create a secret with the creds file contents + contents: + # set secretName in order to mount an existing secret to dir + secretName: + # defaults to /etc/nats-creds/ + dir: + key: nats.creds + nkey: + # set contents in order to create a secret with the nkey file contents + contents: + # set secretName in order to mount an existing secret to dir + secretName: + # defaults to /etc/nats-nkeys/ + dir: + key: nats.nk + # used to connect with client certificates + tls: + # set secretName in order to mount an existing secret to dir + secretName: + # defaults to /etc/nats-certs/ + dir: + cert: tls.crt + key: tls.key + + # merge or patch the context + # https://docs.nats.io/using-nats/nats-tools/nats_cli#nats-contexts + merge: {} + patch: [] + + # name of context to select by default + defaultContextName: default + + ############################################################ + # deployment -> pod template -> nats-box container + ############################################################ + container: + image: + repository: natsio/nats-box + tag: 0.19.3 + pullPolicy: + registry: + digest: + fullImageName: + resources: {} + + # env var map, see nats.env for an example + env: {} + + # merge or patch the container + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#container-v1-core + merge: {} + patch: [] + + ############################################################ + # other nats-box extension points + ############################################################ + + # deployment + deployment: + # merge or patch the deployment + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#deployment-v1-apps + merge: {} + patch: [] + # defaults to "{{ include "nats.fullname" $ }}-box" + name: + + # deployment -> pod template + podTemplate: + # merge or patch the pod template + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#pod-v1-core + merge: {} + patch: [] + + # contexts secret + contextsSecret: + # merge or patch the context secret + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#secret-v1-core + merge: {} + patch: [] + # defaults to "{{ include "nats.fullname" $ }}-box-contexts" + name: + + # contents secret + contentsSecret: + # merge or patch the contents secret + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#secret-v1-core + merge: {} + patch: [] + # defaults to "{{ include "nats.fullname" $ }}-box-contents" + name: + + # service account + serviceAccount: + enabled: false + # merge or patch the service account + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#serviceaccount-v1-core + merge: {} + patch: [] + # defaults to "{{ include "nats.fullname" $ }}-box" + name: + +################################################################################ +# Extra user-defined resources +################################################################################ +# +# add arbitrary user-generated resources +# example: +# +# config: +# websocket: +# enabled: true +# extraResources: +# - apiVersion: networking.istio.io/v1beta1 +# kind: VirtualService +# metadata: +# name: +# $tplYaml: > +# {{ include "nats.fullname" $ | quote }} +# labels: +# $tplYaml: | +# {{ include "nats.labels" $ }} +# spec: +# hosts: +# - demo.nats.io +# gateways: +# - my-gateway +# http: +# - name: default +# match: +# - name: root +# uri: +# exact: / +# route: +# - destination: +# host: +# $tplYaml: > +# {{ .Values.service.name | quote }} +# port: +# number: +# $tplYaml: > +# {{ .Values.config.websocket.port }} +# +extraResources: [] diff --git a/infra/kustomization.yaml b/infra/kustomization.yaml new file mode 100644 index 0000000..d2294b9 --- /dev/null +++ b/infra/kustomization.yaml @@ -0,0 +1,87 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: infra # <-- Add this here instead insidde of benthos-deployment.yaml + +# 1. List your static YAML files here +resources: + #- nats-cluster.yaml + - benthos/benthos-deployment.yaml + #- benthos-service.yaml + +helmCharts: + - name: nats + repo: https://nats-io.github.io/k8s/helm/charts/ + releaseName: nats-cluster + #namespace: infra + #valuesFile: nats-values.yaml # It will look for this file in the same folder + valuesInline: + namespaceOverride: "infra" # if not there got created in default namespaces + replicaCount: 3 + config: + cluster: + enabled: true + jetstream: + enabled: true + fileStore: + pvc: + enabled: true + storageClassName: "longhorn" # Use Longhorn for replication! + size: 5Gi + # Add the nodeSelector directly here instead of using a patch! + nodeSelector: + node-role.kubernetes.io/nworker: "true" + +# 2. List your config sources here +# This tells K8s: "Take every file in these folders and make ConfigMaps" +configMapGenerator: + - name: benthos-streams + files: + - benthos/streams/ingest_request_log_id_serial.yaml + - benthos/streams/ingest_transaction_log_id_serial.yaml + #- streams/poll_request_log_id_serial.yaml + #- streams/poll_transaction_id_serial.yaml + - name: benthos-templates + files: + - benthos/templates/ingest_template_id_serial.yaml + - benthos/templates/poll_template_id_serial.yaml + - name: benthos-resources + files: + - benthos/resources/resources.yaml + +patches: + - target: + kind: Deployment + name: benthos # Ensure this matches the name inside benthos-deployment.yaml + patch: |- + - op: add + path: /spec/template/spec/nodeSelector + value: + node-role.kubernetes.io/nworker: "true" + +# secretGenerator: +# - name: postgres-ha-app +# literals: +# - password=0lkzPxlwj6JVOXwwoLYROZJsONJoPK3MtrqkxnH3iaXUs0gFg0WL78RxyDdB86Sk + # - target: + # kind: StatefulSet + # name: nats-cluster # Ensure this matches the 'metadata.name' in nats-cluster.yaml + # patch: |- + # - op: add + # path: /spec/template/spec/nodeSelector + # value: + # node-role.kubernetes.io/nworker: "true" + +# helmCharts: +# - name: nats +# repo: https://nats-io.github.io/k8s/helm/charts/ +# releaseName: nats-cluster +# namespace: infra +# valuesInline: +# nats: +# jetstream: +# enabled: true +# cluster: +# enabled: true +# replicas: 3 + diff --git a/infra/nats-cluster.yaml b/infra/nats-cluster.yaml new file mode 100644 index 0000000000000000000000000000000000000000..810d308b495284a74e6fa80b61fffc2017a6e0ad GIT binary patch literal 20128 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.21.0-0' +maintainers: +- email: cncf-linkerd-dev@lists.cncf.io + name: Linkerd authors + url: https://linkerd.io/ +name: linkerd-control-plane +sources: +- https://github.com/linkerd/linkerd2/ +type: application +version: 1.16.11 diff --git a/linkerd/charts/linkerd-control-plane/README.md b/linkerd/charts/linkerd-control-plane/README.md new file mode 100644 index 0000000..2de375d --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/README.md @@ -0,0 +1,297 @@ +# linkerd-control-plane + +Linkerd gives you observability, reliability, and security +for your microservices — with no code change required. + +![Version: 1.16.11](https://img.shields.io/badge/Version-1.16.11-informational?style=flat-square) +![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) +![AppVersion: edge-XX.X.X](https://img.shields.io/badge/AppVersion-edge--XX.X.X-informational?style=flat-square) + +**Homepage:** + +## Quickstart and documentation + +You can run Linkerd on any Kubernetes 1.21+ cluster in a matter of seconds. See +the [Linkerd Getting Started Guide][getting-started] for how. + +For more comprehensive documentation, start with the [Linkerd +docs][linkerd-docs]. + +## Prerequisite: linkerd-crds chart + +Before installing this chart, please install the `linkerd-crds` chart, which +creates all the CRDs that the components from the current chart require. + +## Prerequisite: identity certificates + +The identity component of Linkerd requires setting up a trust anchor +certificate, and an issuer certificate with its key. These need to be provided +to Helm by the user (unlike when using the `linkerd install` CLI which can +generate these automatically). You can provide your own, or follow [these +instructions](https://linkerd.io/2/tasks/generate-certificates/) to generate new +ones. + +Alternatively, both trust anchor and identity issuer certificates may be +derived from in-cluster resources. Existing CA (trust anchor) certificates +**must** live in a `ConfigMap` resource named `linkerd-identity-trust-roots`. +Issuer certificates **must** live in a `Secret` named +`linkerd-identity-issuer`. Both resources should exist in the control-plane's +install namespace. In order to use an existing CA, Linkerd needs to be +installed with `identity.externalCA=true`. To use an existing issuer +certificate, Linkerd should be installed with +`identity.issuer.scheme=kubernetes.io/tls`. + +A more comprehensive description is in the [automatic certificate rotation +guide](https://linkerd.io/2.12/tasks/automatically-rotating-control-plane-tls-credentials/#a-note-on-third-party-cert-management-solutions). + +Note that the provided certificates must be ECDSA certificates. + +## Adding Linkerd's Helm repository + +Included here for completeness-sake, but should have already been added when +`linkerd-base` was installed. + +```bash +# To add the repo for Linkerd stable releases: +helm repo add linkerd https://helm.linkerd.io/stable +# To add the repo for Linkerd edge releases: +helm repo add linkerd-edge https://helm.linkerd.io/edge +``` + +The following instructions use the `linkerd` repo. For installing an edge +release, just replace with `linkerd-edge`. + +## Installing the chart + +You must provide the certificates and keys described in the preceding section, +and the same expiration date you used to generate the Issuer certificate. + +```bash +helm install linkerd-control-plane -n linkerd \ + --set-file identityTrustAnchorsPEM=ca.crt \ + --set-file identity.issuer.tls.crtPEM=issuer.crt \ + --set-file identity.issuer.tls.keyPEM=issuer.key \ + linkerd/linkerd-control-plane +``` + +Note that you require to install this chart in the same namespace you installed +the `linkerd-base` chart. + +## Setting High-Availability + +Besides the default `values.yaml` file, the chart provides a `values-ha.yaml` +file that overrides some default values as to set things up under a +high-availability scenario, analogous to the `--ha` option in `linkerd install`. +Values such as higher number of replicas, higher memory/cpu limits and +affinities are specified in that file. + +You can get ahold of `values-ha.yaml` by fetching the chart files: + +```bash +helm fetch --untar linkerd/linkerd-control-plane +``` + +Then use the `-f` flag to provide the override file, for example: + +```bash +helm install linkerd-control-plane -n linkerd \ + --set-file identityTrustAnchorsPEM=ca.crt \ + --set-file identity.issuer.tls.crtPEM=issuer.crt \ + --set-file identity.issuer.tls.keyPEM=issuer.key \ + -f linkerd2/values-ha.yaml + linkerd/linkerd-control-plane +``` + +## Get involved + +* Check out Linkerd's source code at [GitHub][linkerd2]. +* Join Linkerd's [user mailing list][linkerd-users], [developer mailing + list][linkerd-dev], and [announcements mailing list][linkerd-announce]. +* Follow [@linkerd][twitter] on Twitter. +* Join the [Linkerd Slack][slack]. + +[getting-started]: https://linkerd.io/2/getting-started/ +[linkerd2]: https://github.com/linkerd/linkerd2 +[linkerd-announce]: https://lists.cncf.io/g/cncf-linkerd-announce +[linkerd-dev]: https://lists.cncf.io/g/cncf-linkerd-dev +[linkerd-docs]: https://linkerd.io/2/overview/ +[linkerd-users]: https://lists.cncf.io/g/cncf-linkerd-users +[slack]: http://slack.linkerd.io +[twitter]: https://twitter.com/linkerd + +## Extensions for Linkerd + +The current chart installs the core Linkerd components, which grant you +reliability and security features. Other functionality is available through +extensions. Check the corresponding docs for each one of the following +extensions: + +* Observability: + [Linkerd-viz](https://github.com/linkerd/linkerd2/blob/main/viz/charts/linkerd-viz/README.md) +* Multicluster: + [Linkerd-multicluster](https://github.com/linkerd/linkerd2/blob/main/multicluster/charts/linkerd-multicluster/README.md) +* Tracing: + [Linkerd-jaeger](https://github.com/linkerd/linkerd2/blob/main/jaeger/charts/linkerd-jaeger/README.md) + +## Requirements + +Kubernetes: `>=1.21.0-0` + +| Repository | Name | Version | +|------------|------|---------| +| file://../partials | partials | 0.1.0 | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| clusterDomain | string | `"cluster.local"` | Kubernetes DNS Domain name to use | +| clusterNetworks | string | `"10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16"` | The cluster networks for which service discovery is performed. This should include the pod and service networks, but need not include the node network. By default, all private networks are specified so that resolution works in typical Kubernetes environments. | +| cniEnabled | bool | `false` | enabling this omits the NET_ADMIN capability in the PSP and the proxy-init container when injecting the proxy; requires the linkerd-cni plugin to already be installed | +| commonLabels | object | `{}` | Labels to apply to all resources | +| controlPlaneTracing | bool | `false` | enables control plane tracing | +| controlPlaneTracingNamespace | string | `"linkerd-jaeger"` | namespace to send control plane traces to | +| controllerImage | string | `"cr.l5d.io/linkerd/controller"` | Docker image for the destination and identity components | +| controllerImageVersion | string | `""` | Optionally allow a specific container image Tag (or SHA) to be specified for the controllerImage. | +| controllerLogFormat | string | `"plain"` | Log format for the control plane components | +| controllerLogLevel | string | `"info"` | Log level for the control plane components | +| controllerReplicas | int | `1` | Number of replicas for each control plane pod | +| controllerUID | int | `2103` | User ID for the control plane components | +| debugContainer.image.name | string | `"cr.l5d.io/linkerd/debug"` | Docker image for the debug container | +| debugContainer.image.pullPolicy | string | imagePullPolicy | Pull policy for the debug container Docker image | +| debugContainer.image.version | string | linkerdVersion | Tag for the debug container Docker image | +| deploymentStrategy | object | `{"rollingUpdate":{"maxSurge":"25%","maxUnavailable":"25%"}}` | default kubernetes deployment strategy | +| disableHeartBeat | bool | `false` | Set to true to not start the heartbeat cronjob | +| enableEndpointSlices | bool | `true` | enables the use of EndpointSlice informers for the destination service; enableEndpointSlices should be set to true only if EndpointSlice K8s feature gate is on | +| enableH2Upgrade | bool | `true` | Allow proxies to perform transparent HTTP/2 upgrading | +| enablePSP | bool | `false` | Add a PSP resource and bind it to the control plane ServiceAccounts. Note PSP has been deprecated since k8s v1.21 | +| enablePodAntiAffinity | bool | `false` | enables pod anti affinity creation on deployments for high availability | +| enablePodDisruptionBudget | bool | `false` | enables the creation of pod disruption budgets for control plane components | +| enablePprof | bool | `false` | enables the use of pprof endpoints on control plane component's admin servers | +| identity.externalCA | bool | `false` | If the linkerd-identity-trust-roots ConfigMap has already been created | +| identity.issuer.clockSkewAllowance | string | `"20s"` | Amount of time to allow for clock skew within a Linkerd cluster | +| identity.issuer.issuanceLifetime | string | `"24h0m0s"` | Amount of time for which the Identity issuer should certify identity | +| identity.issuer.scheme | string | `"linkerd.io/tls"` | | +| identity.issuer.tls | object | `{"crtPEM":"","keyPEM":""}` | Which scheme is used for the identity issuer secret format | +| identity.issuer.tls.crtPEM | string | `""` | Issuer certificate (ECDSA). It must be provided during install. | +| identity.issuer.tls.keyPEM | string | `""` | Key for the issuer certificate (ECDSA). It must be provided during install | +| identity.kubeAPI.clientBurst | int | `200` | Burst value over clientQPS | +| identity.kubeAPI.clientQPS | int | `100` | Maximum QPS sent to the kube-apiserver before throttling. See [token bucket rate limiter implementation](https://github.com/kubernetes/client-go/blob/v12.0.0/util/flowcontrol/throttle.go) | +| identity.serviceAccountTokenProjection | bool | `true` | Use [Service Account token Volume projection](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#service-account-token-volume-projection) for pod validation instead of the default token | +| identityTrustAnchorsPEM | string | `""` | Trust root certificate (ECDSA). It must be provided during install. | +| identityTrustDomain | string | clusterDomain | Trust domain used for identity | +| imagePullPolicy | string | `"IfNotPresent"` | Docker image pull policy | +| imagePullSecrets | list | `[]` | For Private docker registries, authentication is needed. Registry secrets are applied to the respective service accounts | +| kubeAPI.clientBurst | int | `200` | Burst value over clientQPS | +| kubeAPI.clientQPS | int | `100` | Maximum QPS sent to the kube-apiserver before throttling. See [token bucket rate limiter implementation](https://github.com/kubernetes/client-go/blob/v12.0.0/util/flowcontrol/throttle.go) | +| linkerdVersion | string | `"linkerdVersionValue"` | control plane version. See Proxy section for proxy version | +| networkValidator.connectAddr | string | `"1.1.1.1:20001"` | Address to which the network-validator will attempt to connect. we expect this to be rewritten | +| networkValidator.enableSecurityContext | bool | `true` | Include a securityContext in the network-validator pod spec | +| networkValidator.listenAddr | string | `"0.0.0.0:4140"` | Address to which network-validator listens to requests from itself | +| networkValidator.logFormat | string | plain | Log format (`plain` or `json`) for network-validator | +| networkValidator.logLevel | string | debug | Log level for the network-validator | +| networkValidator.timeout | string | `"10s"` | Timeout before network-validator fails to validate the pod's network connectivity | +| nodeSelector | object | `{"kubernetes.io/os":"linux"}` | NodeSelector section, See the [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information | +| podAnnotations | object | `{}` | Additional annotations to add to all pods | +| podLabels | object | `{}` | Additional labels to add to all pods | +| podMonitor.controller.enabled | bool | `true` | Enables the creation of PodMonitor for the control-plane | +| podMonitor.controller.namespaceSelector | string | `"matchNames:\n - {{ .Release.Namespace }}\n - linkerd-viz\n - linkerd-jaeger\n"` | Selector to select which namespaces the Endpoints objects are discovered from | +| podMonitor.enabled | bool | `false` | Enables the creation of Prometheus Operator [PodMonitor](https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1.PodMonitor) | +| podMonitor.labels | object | `{}` | Labels to apply to all pod Monitors | +| podMonitor.proxy.enabled | bool | `true` | Enables the creation of PodMonitor for the data-plane | +| podMonitor.scrapeInterval | string | `"10s"` | Interval at which metrics should be scraped | +| podMonitor.scrapeTimeout | string | `"10s"` | Iimeout after which the scrape is ended | +| podMonitor.serviceMirror.enabled | bool | `true` | Enables the creation of PodMonitor for the Service Mirror component | +| policyController.image.name | string | `"cr.l5d.io/linkerd/policy-controller"` | Docker image for the policy controller | +| policyController.image.pullPolicy | string | imagePullPolicy | Pull policy for the proxy container Docker image | +| policyController.image.version | string | linkerdVersion | Tag for the proxy container Docker image | +| policyController.logLevel | string | `"info"` | Log level for the policy controller | +| policyController.probeNetworks | list | `["0.0.0.0/0"]` | The networks from which probes are performed. By default, all networks are allowed so that all probes are authorized. | +| policyController.resources | object | destinationResources | policy controller resource requests & limits | +| policyController.resources.cpu.limit | string | `""` | Maximum amount of CPU units that the policy controller can use | +| policyController.resources.cpu.request | string | `""` | Amount of CPU units that the policy controller requests | +| policyController.resources.ephemeral-storage.limit | string | `""` | Maximum amount of ephemeral storage that the policy controller can use | +| policyController.resources.ephemeral-storage.request | string | `""` | Amount of ephemeral storage that the policy controller requests | +| policyController.resources.memory.limit | string | `""` | Maximum amount of memory that the policy controller can use | +| policyController.resources.memory.request | string | `""` | Maximum amount of memory that the policy controller requests | +| policyValidator.caBundle | string | `""` | Bundle of CA certificates for proxy injector. If not provided nor injected with cert-manager, then Helm will use the certificate generated for `policyValidator.crtPEM`. If `policyValidator.externalSecret` is set to true, this value, injectCaFrom, or injectCaFromSecret must be set, as no certificate will be generated. See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector) for more information. | +| policyValidator.crtPEM | string | `""` | Certificate for the policy validator. If not provided and not using an external secret then Helm will generate one. | +| policyValidator.externalSecret | bool | `false` | Do not create a secret resource for the policyValidator webhook. If this is set to `true`, the value `policyValidator.caBundle` must be set or the ca bundle must injected with cert-manager ca injector using `policyValidator.injectCaFrom` or `policyValidator.injectCaFromSecret` (see below). | +| policyValidator.injectCaFrom | string | `""` | Inject the CA bundle from a cert-manager Certificate. See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-certificate-resource) for more information. | +| policyValidator.injectCaFromSecret | string | `""` | Inject the CA bundle from a Secret. If set, the `cert-manager.io/inject-ca-from-secret` annotation will be added to the webhook. The Secret must have the CA Bundle stored in the `ca.crt` key and have the `cert-manager.io/allow-direct-injection` annotation set to `true`. See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-secret-resource) for more information. | +| policyValidator.keyPEM | string | `""` | Certificate key for the policy validator. If not provided and not using an external secret then Helm will generate one. | +| policyValidator.namespaceSelector | object | `{"matchExpressions":[{"key":"config.linkerd.io/admission-webhooks","operator":"NotIn","values":["disabled"]}]}` | Namespace selector used by admission webhook | +| priorityClassName | string | `""` | Kubernetes priorityClassName for the Linkerd Pods | +| profileValidator.caBundle | string | `""` | Bundle of CA certificates for proxy injector. If not provided nor injected with cert-manager, then Helm will use the certificate generated for `profileValidator.crtPEM`. If `profileValidator.externalSecret` is set to true, this value, injectCaFrom, or injectCaFromSecret must be set, as no certificate will be generated. See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector) for more information. | +| profileValidator.crtPEM | string | `""` | Certificate for the service profile validator. If not provided and not using an external secret then Helm will generate one. | +| profileValidator.externalSecret | bool | `false` | Do not create a secret resource for the profileValidator webhook. If this is set to `true`, the value `proxyInjector.caBundle` must be set or the ca bundle must injected with cert-manager ca injector using `proxyInjector.injectCaFrom` or `proxyInjector.injectCaFromSecret` (see below). | +| profileValidator.injectCaFrom | string | `""` | Inject the CA bundle from a cert-manager Certificate. See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-certificate-resource) for more information. | +| profileValidator.injectCaFromSecret | string | `""` | Inject the CA bundle from a Secret. If set, the `cert-manager.io/inject-ca-from-secret` annotation will be added to the webhook. The Secret must have the CA Bundle stored in the `ca.crt` key and have the `cert-manager.io/allow-direct-injection` annotation set to `true`. See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-secret-resource) for more information. | +| profileValidator.keyPEM | string | `""` | Certificate key for the service profile validator. If not provided and not using an external secret then Helm will generate one. | +| profileValidator.namespaceSelector | object | `{"matchExpressions":[{"key":"config.linkerd.io/admission-webhooks","operator":"NotIn","values":["disabled"]}]}` | Namespace selector used by admission webhook | +| prometheusUrl | string | `""` | url of external prometheus instance (used for the heartbeat) | +| proxy.await | bool | `true` | If set, the application container will not start until the proxy is ready | +| proxy.cores | int | `0` | The `cpu.limit` and `cores` should be kept in sync. The value of `cores` must be an integer and should typically be set by rounding up from the limit. E.g. if cpu.limit is '1500m', cores should be 2. | +| proxy.defaultInboundPolicy | string | "all-unauthenticated" | The default allow policy to use when no `Server` selects a pod. One of: "all-authenticated", "all-unauthenticated", "cluster-authenticated", "cluster-unauthenticated", "deny" | +| proxy.disableInboundProtocolDetectTimeout | bool | `false` | When set to true, disables the protocol detection timeout on the inbound side of the proxy by setting it to a very high value | +| proxy.disableOutboundProtocolDetectTimeout | bool | `false` | When set to true, disables the protocol detection timeout on the outbound side of the proxy by setting it to a very high value | +| proxy.enableExternalProfiles | bool | `false` | Enable service profiles for non-Kubernetes services | +| proxy.image.name | string | `"cr.l5d.io/linkerd/proxy"` | Docker image for the proxy | +| proxy.image.pullPolicy | string | imagePullPolicy | Pull policy for the proxy container Docker image | +| proxy.image.version | string | linkerdVersion | Tag for the proxy container Docker image | +| proxy.inboundConnectTimeout | string | `"100ms"` | Maximum time allowed for the proxy to establish an inbound TCP connection | +| proxy.inboundDiscoveryCacheUnusedTimeout | string | `"90s"` | Maximum time allowed before an unused inbound discovery result is evicted from the cache | +| proxy.logFormat | string | `"plain"` | Log format (`plain` or `json`) for the proxy | +| proxy.logLevel | string | `"warn,linkerd=info,trust_dns=error"` | Log level for the proxy | +| proxy.opaquePorts | string | `"25,587,3306,4444,5432,6379,9300,11211"` | Default set of opaque ports - SMTP (25,587) server-first - MYSQL (3306) server-first - Galera (4444) server-first - PostgreSQL (5432) server-first - Redis (6379) server-first - ElasticSearch (9300) server-first - Memcached (11211) clients do not issue any preamble, which breaks detection | +| proxy.outboundConnectTimeout | string | `"1000ms"` | Maximum time allowed for the proxy to establish an outbound TCP connection | +| proxy.outboundDiscoveryCacheUnusedTimeout | string | `"5s"` | Maximum time allowed before an unused outbound discovery result is evicted from the cache | +| proxy.ports.admin | int | `4191` | Admin port for the proxy container | +| proxy.ports.control | int | `4190` | Control port for the proxy container | +| proxy.ports.inbound | int | `4143` | Inbound port for the proxy container | +| proxy.ports.outbound | int | `4140` | Outbound port for the proxy container | +| proxy.requireIdentityOnInboundPorts | string | `""` | | +| proxy.resources.cpu.limit | string | `""` | Maximum amount of CPU units that the proxy can use | +| proxy.resources.cpu.request | string | `""` | Amount of CPU units that the proxy requests | +| proxy.resources.ephemeral-storage.limit | string | `""` | Maximum amount of ephemeral storage that the proxy can use | +| proxy.resources.ephemeral-storage.request | string | `""` | Amount of ephemeral storage that the proxy requests | +| proxy.resources.memory.limit | string | `""` | Maximum amount of memory that the proxy can use | +| proxy.resources.memory.request | string | `""` | Maximum amount of memory that the proxy requests | +| proxy.shutdownGracePeriod | string | `""` | Grace period for graceful proxy shutdowns. If this timeout elapses before all open connections have completed, the proxy will terminate forcefully, closing any remaining connections. | +| proxy.uid | int | `2102` | User id under which the proxy runs | +| proxy.waitBeforeExitSeconds | int | `0` | If set the injected proxy sidecars in the data plane will stay alive for at least the given period before receiving the SIGTERM signal from Kubernetes but no longer than the pod's `terminationGracePeriodSeconds`. See [Lifecycle hooks](https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks) for more info on container lifecycle hooks. | +| proxyInit.closeWaitTimeoutSecs | int | `0` | | +| proxyInit.ignoreInboundPorts | string | `"4567,4568"` | Default set of inbound ports to skip via iptables - Galera (4567,4568) | +| proxyInit.ignoreOutboundPorts | string | `"4567,4568"` | Default set of outbound ports to skip via iptables - Galera (4567,4568) | +| proxyInit.image.name | string | `"cr.l5d.io/linkerd/proxy-init"` | Docker image for the proxy-init container | +| proxyInit.image.pullPolicy | string | imagePullPolicy | Pull policy for the proxy-init container Docker image | +| proxyInit.image.version | string | `"v2.2.3"` | Tag for the proxy-init container Docker image | +| proxyInit.iptablesMode | string | `"legacy"` | Variant of iptables that will be used to configure routing. Currently, proxy-init can be run either in 'nft' or in 'legacy' mode. The mode will control which utility binary will be called. The host must support whichever mode will be used | +| proxyInit.kubeAPIServerPorts | string | `"443,6443"` | Default set of ports to skip via iptables for control plane components so they can communicate with the Kubernetes API Server | +| proxyInit.logFormat | string | plain | Log format (`plain` or `json`) for the proxy-init | +| proxyInit.logLevel | string | info | Log level for the proxy-init | +| proxyInit.privileged | bool | false | Privileged mode allows the container processes to inherit all security capabilities and bypass any security limitations enforced by the kubelet. When used with 'runAsRoot: true', the container will behave exactly as if it was running as root on the host. May escape cgroup limits and see other processes and devices on the host. | +| proxyInit.resources.cpu.limit | string | `"100m"` | Maximum amount of CPU units that the proxy-init container can use | +| proxyInit.resources.cpu.request | string | `"100m"` | Amount of CPU units that the proxy-init container requests | +| proxyInit.resources.ephemeral-storage.limit | string | `""` | Maximum amount of ephemeral storage that the proxy-init container can use | +| proxyInit.resources.ephemeral-storage.request | string | `""` | Amount of ephemeral storage that the proxy-init container requests | +| proxyInit.resources.memory.limit | string | `"20Mi"` | Maximum amount of memory that the proxy-init container can use | +| proxyInit.resources.memory.request | string | `"20Mi"` | Amount of memory that the proxy-init container requests | +| proxyInit.runAsRoot | bool | `false` | Allow overriding the runAsNonRoot behaviour () | +| proxyInit.runAsUser | int | `65534` | This value is used only if runAsRoot is false; otherwise runAsUser will be 0 | +| proxyInit.skipSubnets | string | `""` | Comma-separated list of subnets in valid CIDR format that should be skipped by the proxy | +| proxyInit.xtMountPath.mountPath | string | `"/run"` | | +| proxyInit.xtMountPath.name | string | `"linkerd-proxy-init-xtables-lock"` | | +| proxyInjector.caBundle | string | `""` | Bundle of CA certificates for proxy injector. If not provided nor injected with cert-manager, then Helm will use the certificate generated for `proxyInjector.crtPEM`. If `proxyInjector.externalSecret` is set to true, this value, injectCaFrom, or injectCaFromSecret must be set, as no certificate will be generated. See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector) for more information. | +| proxyInjector.crtPEM | string | `""` | Certificate for the proxy injector. If not provided and not using an external secret then Helm will generate one. | +| proxyInjector.externalSecret | bool | `false` | Do not create a secret resource for the proxyInjector webhook. If this is set to `true`, the value `proxyInjector.caBundle` must be set or the ca bundle must injected with cert-manager ca injector using `proxyInjector.injectCaFrom` or `proxyInjector.injectCaFromSecret` (see below). | +| proxyInjector.injectCaFrom | string | `""` | Inject the CA bundle from a cert-manager Certificate. See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-certificate-resource) for more information. | +| proxyInjector.injectCaFromSecret | string | `""` | Inject the CA bundle from a Secret. If set, the `cert-manager.io/inject-ca-from-secret` annotation will be added to the webhook. The Secret must have the CA Bundle stored in the `ca.crt` key and have the `cert-manager.io/allow-direct-injection` annotation set to `true`. See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-secret-resource) for more information. | +| proxyInjector.keyPEM | string | `""` | Certificate key for the proxy injector. If not provided and not using an external secret then Helm will generate one. | +| proxyInjector.namespaceSelector | object | `{"matchExpressions":[{"key":"config.linkerd.io/admission-webhooks","operator":"NotIn","values":["disabled"]},{"key":"kubernetes.io/metadata.name","operator":"NotIn","values":["kube-system","cert-manager"]}]}` | Namespace selector used by admission webhook. | +| proxyInjector.objectSelector | object | `{"matchExpressions":[{"key":"linkerd.io/control-plane-component","operator":"DoesNotExist"},{"key":"linkerd.io/cni-resource","operator":"DoesNotExist"}]}` | Object selector used by admission webhook. | +| runtimeClassName | string | `""` | Runtime Class Name for all the pods | +| webhookFailurePolicy | string | `"Ignore"` | Failure policy for the proxy injector | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/linkerd/charts/linkerd-control-plane/README.md.gotmpl b/linkerd/charts/linkerd-control-plane/README.md.gotmpl new file mode 100644 index 0000000..2d96c3d --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/README.md.gotmpl @@ -0,0 +1,138 @@ +{{ template "chart.header" . }} +{{ template "chart.description" . }} + +{{ template "chart.versionBadge" . }} +{{ template "chart.typeBadge" . }} +{{ template "chart.appVersionBadge" . }} + +{{ template "chart.homepageLine" . }} + +## Quickstart and documentation + +You can run Linkerd on any Kubernetes 1.21+ cluster in a matter of seconds. See +the [Linkerd Getting Started Guide][getting-started] for how. + +For more comprehensive documentation, start with the [Linkerd +docs][linkerd-docs]. + +## Prerequisite: linkerd-crds chart + +Before installing this chart, please install the `linkerd-crds` chart, which +creates all the CRDs that the components from the current chart require. + +## Prerequisite: identity certificates + +The identity component of Linkerd requires setting up a trust anchor +certificate, and an issuer certificate with its key. These need to be provided +to Helm by the user (unlike when using the `linkerd install` CLI which can +generate these automatically). You can provide your own, or follow [these +instructions](https://linkerd.io/2/tasks/generate-certificates/) to generate new +ones. + +Alternatively, both trust anchor and identity issuer certificates may be +derived from in-cluster resources. Existing CA (trust anchor) certificates +**must** live in a `ConfigMap` resource named `linkerd-identity-trust-roots`. +Issuer certificates **must** live in a `Secret` named +`linkerd-identity-issuer`. Both resources should exist in the control-plane's +install namespace. In order to use an existing CA, Linkerd needs to be +installed with `identity.externalCA=true`. To use an existing issuer +certificate, Linkerd should be installed with +`identity.issuer.scheme=kubernetes.io/tls`. + +A more comprehensive description is in the [automatic certificate rotation +guide](https://linkerd.io/2.12/tasks/automatically-rotating-control-plane-tls-credentials/#a-note-on-third-party-cert-management-solutions). + +Note that the provided certificates must be ECDSA certificates. + +## Adding Linkerd's Helm repository + +Included here for completeness-sake, but should have already been added when +`linkerd-base` was installed. + +```bash +# To add the repo for Linkerd stable releases: +helm repo add linkerd https://helm.linkerd.io/stable +# To add the repo for Linkerd edge releases: +helm repo add linkerd-edge https://helm.linkerd.io/edge +``` + +The following instructions use the `linkerd` repo. For installing an edge +release, just replace with `linkerd-edge`. + +## Installing the chart + +You must provide the certificates and keys described in the preceding section, +and the same expiration date you used to generate the Issuer certificate. + +```bash +helm install linkerd-control-plane -n linkerd \ + --set-file identityTrustAnchorsPEM=ca.crt \ + --set-file identity.issuer.tls.crtPEM=issuer.crt \ + --set-file identity.issuer.tls.keyPEM=issuer.key \ + linkerd/linkerd-control-plane +``` + +Note that you require to install this chart in the same namespace you installed +the `linkerd-base` chart. + +## Setting High-Availability + +Besides the default `values.yaml` file, the chart provides a `values-ha.yaml` +file that overrides some default values as to set things up under a +high-availability scenario, analogous to the `--ha` option in `linkerd install`. +Values such as higher number of replicas, higher memory/cpu limits and +affinities are specified in that file. + +You can get ahold of `values-ha.yaml` by fetching the chart files: + +```bash +helm fetch --untar linkerd/linkerd-control-plane +``` + +Then use the `-f` flag to provide the override file, for example: + +```bash +helm install linkerd-control-plane -n linkerd \ + --set-file identityTrustAnchorsPEM=ca.crt \ + --set-file identity.issuer.tls.crtPEM=issuer.crt \ + --set-file identity.issuer.tls.keyPEM=issuer.key \ + -f linkerd2/values-ha.yaml + linkerd/linkerd-control-plane +``` + +## Get involved + +* Check out Linkerd's source code at [GitHub][linkerd2]. +* Join Linkerd's [user mailing list][linkerd-users], [developer mailing + list][linkerd-dev], and [announcements mailing list][linkerd-announce]. +* Follow [@linkerd][twitter] on Twitter. +* Join the [Linkerd Slack][slack]. + +[getting-started]: https://linkerd.io/2/getting-started/ +[linkerd2]: https://github.com/linkerd/linkerd2 +[linkerd-announce]: https://lists.cncf.io/g/cncf-linkerd-announce +[linkerd-dev]: https://lists.cncf.io/g/cncf-linkerd-dev +[linkerd-docs]: https://linkerd.io/2/overview/ +[linkerd-users]: https://lists.cncf.io/g/cncf-linkerd-users +[slack]: http://slack.linkerd.io +[twitter]: https://twitter.com/linkerd + +## Extensions for Linkerd + +The current chart installs the core Linkerd components, which grant you +reliability and security features. Other functionality is available through +extensions. Check the corresponding docs for each one of the following +extensions: + +* Observability: + [Linkerd-viz](https://github.com/linkerd/linkerd2/blob/main/viz/charts/linkerd-viz/README.md) +* Multicluster: + [Linkerd-multicluster](https://github.com/linkerd/linkerd2/blob/main/multicluster/charts/linkerd-multicluster/README.md) +* Tracing: + [Linkerd-jaeger](https://github.com/linkerd/linkerd2/blob/main/jaeger/charts/linkerd-jaeger/README.md) + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/.helmignore b/linkerd/charts/linkerd-control-plane/charts/partials/.helmignore new file mode 100644 index 0000000..f0c1319 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/charts/partials/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/Chart.yaml b/linkerd/charts/linkerd-control-plane/charts/partials/Chart.yaml new file mode 100644 index 0000000..23cfc16 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/charts/partials/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +description: 'A Helm chart containing Linkerd partial templates, depended by the ''linkerd'' + and ''patch'' charts. ' +name: partials +version: 0.1.0 diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/README.md b/linkerd/charts/linkerd-control-plane/charts/partials/README.md new file mode 100644 index 0000000..e5fbd39 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/charts/partials/README.md @@ -0,0 +1,9 @@ +# partials + +A Helm chart containing Linkerd partial templates, +depended by the 'linkerd' and 'patch' charts. + +![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/README.md.gotmpl b/linkerd/charts/linkerd-control-plane/charts/partials/README.md.gotmpl new file mode 100644 index 0000000..37f5101 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/charts/partials/README.md.gotmpl @@ -0,0 +1,14 @@ +{{ template "chart.header" . }} +{{ template "chart.description" . }} + +{{ template "chart.versionBadge" . }} +{{ template "chart.typeBadge" . }} +{{ template "chart.appVersionBadge" . }} + +{{ template "chart.homepageLine" . }} + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/templates/NOTES.txt b/linkerd/charts/linkerd-control-plane/charts/partials/templates/NOTES.txt new file mode 100644 index 0000000..e69de29 diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/templates/_affinity.tpl b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_affinity.tpl new file mode 100644 index 0000000..5dde1da --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_affinity.tpl @@ -0,0 +1,38 @@ +{{ define "linkerd.pod-affinity" -}} +podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: {{ default "linkerd.io/control-plane-component" .label }} + operator: In + values: + - {{ .component }} + topologyKey: topology.kubernetes.io/zone + weight: 100 + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: {{ default "linkerd.io/control-plane-component" .label }} + operator: In + values: + - {{ .component }} + topologyKey: kubernetes.io/hostname +{{- end }} + +{{ define "linkerd.node-affinity" -}} +nodeAffinity: +{{- toYaml .Values.nodeAffinity | trim | nindent 2 }} +{{- end }} + +{{ define "linkerd.affinity" -}} +{{- if or .Values.enablePodAntiAffinity .Values.nodeAffinity -}} +affinity: +{{- end }} +{{- if .Values.enablePodAntiAffinity -}} +{{- include "linkerd.pod-affinity" . | nindent 2 }} +{{- end }} +{{- if .Values.nodeAffinity -}} +{{- include "linkerd.node-affinity" . | nindent 2 }} +{{- end }} +{{- end }} diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/templates/_capabilities.tpl b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_capabilities.tpl new file mode 100644 index 0000000..a595d74 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_capabilities.tpl @@ -0,0 +1,16 @@ +{{- define "partials.proxy.capabilities" -}} +capabilities: + {{- if .Values.proxy.capabilities.add }} + add: + {{- toYaml .Values.proxy.capabilities.add | trim | nindent 4 }} + {{- end }} + {{- if .Values.proxy.capabilities.drop }} + drop: + {{- toYaml .Values.proxy.capabilities.drop | trim | nindent 4 }} + {{- end }} +{{- end -}} + +{{- define "partials.proxy-init.capabilities.drop" -}} +drop: +{{ toYaml .Values.proxyInit.capabilities.drop | trim }} +{{- end -}} diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/templates/_debug.tpl b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_debug.tpl new file mode 100644 index 0000000..cf0eb14 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_debug.tpl @@ -0,0 +1,6 @@ +{{- define "partials.debug" -}} +image: {{.Values.debugContainer.image.name}}:{{.Values.debugContainer.image.version | default .Values.linkerdVersion}} +imagePullPolicy: {{.Values.debugContainer.image.pullPolicy | default .Values.imagePullPolicy}} +name: linkerd-debug +terminationMessagePolicy: FallbackToLogsOnError +{{- end -}} diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/templates/_helpers.tpl b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_helpers.tpl new file mode 100644 index 0000000..b6cdc34 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_helpers.tpl @@ -0,0 +1,14 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Splits a coma separated list into a list of string values. +For example "11,22,55,44" will become "11","22","55","44" +*/}} +{{- define "partials.splitStringList" -}} +{{- if gt (len (toString .)) 0 -}} +{{- $ports := toString . | splitList "," -}} +{{- $last := sub (len $ports) 1 -}} +{{- range $i,$port := $ports -}} +"{{$port}}"{{ternary "," "" (ne $i $last)}} +{{- end -}} +{{- end -}} +{{- end -}} diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/templates/_metadata.tpl b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_metadata.tpl new file mode 100644 index 0000000..04d2f1b --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_metadata.tpl @@ -0,0 +1,17 @@ +{{- define "partials.annotations.created-by" -}} +linkerd.io/created-by: {{ .Values.cliVersion | default (printf "linkerd/helm %s" ( (.Values.image).version | default .Values.linkerdVersion)) }} +{{- end -}} + +{{- define "partials.proxy.annotations" -}} +linkerd.io/proxy-version: {{.Values.proxy.image.version | default .Values.linkerdVersion}} +cluster-autoscaler.kubernetes.io/safe-to-evict: "true" +linkerd.io/trust-root-sha256: {{ .Values.identityTrustAnchorsPEM | sha256sum }} +{{- end -}} + +{{/* +To add labels to the control-plane components, instead update at individual component manifests as +adding here would also update `spec.selector.matchLabels` which are immutable and would fail upgrades. +*/}} +{{- define "partials.proxy.labels" -}} +linkerd.io/proxy-{{.workloadKind}}: {{.component}} +{{- end -}} diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/templates/_network-validator.tpl b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_network-validator.tpl new file mode 100644 index 0000000..58f36e6 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_network-validator.tpl @@ -0,0 +1,32 @@ +{{- define "partials.network-validator" -}} +name: linkerd-network-validator +image: {{.Values.proxy.image.name}}:{{.Values.proxy.image.version | default .Values.linkerdVersion }} +imagePullPolicy: {{.Values.proxy.image.pullPolicy | default .Values.imagePullPolicy}} +{{ include "partials.resources" .Values.proxyInit.resources }} +{{- if or .Values.networkValidator.enableSecurityContext }} +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 65534 + seccompProfile: + type: RuntimeDefault +{{- end }} +command: + - /usr/lib/linkerd/linkerd2-network-validator +args: + - --log-format + - {{ .Values.networkValidator.logFormat }} + - --log-level + - {{ .Values.networkValidator.logLevel }} + - --connect-addr + - {{ .Values.networkValidator.connectAddr }} + - --listen-addr + - {{ .Values.networkValidator.listenAddr }} + - --timeout + - {{ .Values.networkValidator.timeout }} + +{{- end -}} diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/templates/_nodeselector.tpl b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_nodeselector.tpl new file mode 100644 index 0000000..4cde0ab --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_nodeselector.tpl @@ -0,0 +1,4 @@ +{{- define "linkerd.node-selector" -}} +nodeSelector: +{{- toYaml .Values.nodeSelector | trim | nindent 2 }} +{{- end -}} diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/templates/_proxy-config-ann.tpl b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_proxy-config-ann.tpl new file mode 100644 index 0000000..9651b3b --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_proxy-config-ann.tpl @@ -0,0 +1,18 @@ +{{- define "partials.proxy.config.annotations" -}} +{{- with .cpu }} +{{- with .request -}} +config.linkerd.io/proxy-cpu-request: {{. | quote}} +{{end}} +{{- with .limit -}} +config.linkerd.io/proxy-cpu-limit: {{. | quote}} +{{- end}} +{{- end}} +{{- with .memory }} +{{- with .request }} +config.linkerd.io/proxy-memory-request: {{. | quote}} +{{end}} +{{- with .limit -}} +config.linkerd.io/proxy-memory-limit: {{. | quote}} +{{- end}} +{{- end }} +{{- end }} diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/templates/_proxy-init.tpl b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_proxy-init.tpl new file mode 100644 index 0000000..91cc96e --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_proxy-init.tpl @@ -0,0 +1,89 @@ +{{- define "partials.proxy-init" -}} +args: +{{- if (.Values.proxyInit.iptablesMode | default "legacy" | eq "nft") }} +- --firewall-bin-path +- "iptables-nft" +- --firewall-save-bin-path +- "iptables-nft-save" +{{- else if not (eq .Values.proxyInit.iptablesMode "legacy") }} +{{ fail (printf "Unsupported value \"%s\" for proxyInit.iptablesMode\nValid values: [\"nft\", \"legacy\"]" .Values.proxyInit.iptablesMode) }} +{{- end }} +- --incoming-proxy-port +- {{.Values.proxy.ports.inbound | quote}} +- --outgoing-proxy-port +- {{.Values.proxy.ports.outbound | quote}} +- --proxy-uid +- {{.Values.proxy.uid | quote}} +- --inbound-ports-to-ignore +- "{{.Values.proxy.ports.control}},{{.Values.proxy.ports.admin}}{{ternary (printf ",%s" (.Values.proxyInit.ignoreInboundPorts | toString)) "" (not (empty .Values.proxyInit.ignoreInboundPorts)) }}" +{{- if .Values.proxyInit.ignoreOutboundPorts }} +- --outbound-ports-to-ignore +- {{.Values.proxyInit.ignoreOutboundPorts | quote}} +{{- end }} +{{- if .Values.proxyInit.closeWaitTimeoutSecs }} +- --timeout-close-wait-secs +- {{ .Values.proxyInit.closeWaitTimeoutSecs | quote}} +{{- end }} +{{- if .Values.proxyInit.logFormat }} +- --log-format +- {{ .Values.proxyInit.logFormat }} +{{- end }} +{{- if .Values.proxyInit.logLevel }} +- --log-level +- {{ .Values.proxyInit.logLevel }} +{{- end }} +{{- if .Values.proxyInit.skipSubnets }} +- --subnets-to-ignore +- {{ .Values.proxyInit.skipSubnets | quote }} +{{- end }} +image: {{.Values.proxyInit.image.name}}:{{.Values.proxyInit.image.version}} +imagePullPolicy: {{.Values.proxyInit.image.pullPolicy | default .Values.imagePullPolicy}} +name: linkerd-init +{{ include "partials.resources" .Values.proxyInit.resources }} +securityContext: + {{- if or .Values.proxyInit.closeWaitTimeoutSecs .Values.proxyInit.privileged }} + allowPrivilegeEscalation: true + {{- else }} + allowPrivilegeEscalation: false + {{- end }} + capabilities: + add: + - NET_ADMIN + - NET_RAW + {{- if .Values.proxyInit.capabilities -}} + {{- if .Values.proxyInit.capabilities.add }} + {{- toYaml .Values.proxyInit.capabilities.add | trim | nindent 4 }} + {{- end }} + {{- if .Values.proxyInit.capabilities.drop -}} + {{- include "partials.proxy-init.capabilities.drop" . | nindent 4 -}} + {{- end }} + {{- end }} + {{- if or .Values.proxyInit.closeWaitTimeoutSecs .Values.proxyInit.privileged }} + privileged: true + {{- else }} + privileged: false + {{- end }} + {{- if .Values.proxyInit.runAsRoot }} + runAsNonRoot: false + runAsUser: 0 + {{- else }} + runAsNonRoot: true + runAsUser: {{ .Values.proxyInit.runAsUser | int | eq 0 | ternary 65534 .Values.proxyInit.runAsUser }} + {{- end }} + readOnlyRootFilesystem: true + seccompProfile: + type: RuntimeDefault +terminationMessagePolicy: FallbackToLogsOnError +{{- if or (not .Values.cniEnabled) .Values.proxyInit.saMountPath }} +volumeMounts: +{{- end -}} +{{- if not .Values.cniEnabled }} +- mountPath: {{.Values.proxyInit.xtMountPath.mountPath}} + name: {{.Values.proxyInit.xtMountPath.name}} +{{- end -}} +{{- if .Values.proxyInit.saMountPath }} +- mountPath: {{.Values.proxyInit.saMountPath.mountPath}} + name: {{.Values.proxyInit.saMountPath.name}} + readOnly: {{.Values.proxyInit.saMountPath.readOnly}} +{{- end -}} +{{- end -}} diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/templates/_proxy.tpl b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_proxy.tpl new file mode 100644 index 0000000..f5dd4c2 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_proxy.tpl @@ -0,0 +1,215 @@ +{{ define "partials.proxy" -}} +{{- $trustDomain := (.Values.identityTrustDomain | default .Values.clusterDomain) -}} +env: +- name: _pod_name + valueFrom: + fieldRef: + fieldPath: metadata.name +- name: _pod_ns + valueFrom: + fieldRef: + fieldPath: metadata.namespace +- name: _pod_nodeName + valueFrom: + fieldRef: + fieldPath: spec.nodeName +{{- if .Values.proxy.cores }} +- name: LINKERD2_PROXY_CORES + value: {{.Values.proxy.cores | quote}} +{{- end }} +{{ if .Values.proxy.requireIdentityOnInboundPorts -}} +- name: LINKERD2_PROXY_INBOUND_PORTS_REQUIRE_IDENTITY + value: {{.Values.proxy.requireIdentityOnInboundPorts | quote}} +{{ end -}} +{{ if .Values.proxy.requireTLSOnInboundPorts -}} +- name: LINKERD2_PROXY_INBOUND_PORTS_REQUIRE_TLS + value: {{.Values.proxy.requireTLSOnInboundPorts | quote}} +{{ end -}} +- name: LINKERD2_PROXY_LOG + value: {{.Values.proxy.logLevel | quote}} +- name: LINKERD2_PROXY_LOG_FORMAT + value: {{.Values.proxy.logFormat | quote}} +- name: LINKERD2_PROXY_DESTINATION_SVC_ADDR + value: {{ternary "localhost.:8086" (printf "linkerd-dst-headless.%s.svc.%s.:8086" .Release.Namespace .Values.clusterDomain) (eq (toString .Values.proxy.component) "linkerd-destination")}} +- name: LINKERD2_PROXY_DESTINATION_PROFILE_NETWORKS + value: {{.Values.clusterNetworks | quote}} +- name: LINKERD2_PROXY_POLICY_SVC_ADDR + value: {{ternary "localhost.:8090" (printf "linkerd-policy.%s.svc.%s.:8090" .Release.Namespace .Values.clusterDomain) (eq (toString .Values.proxy.component) "linkerd-destination")}} +- name: LINKERD2_PROXY_POLICY_WORKLOAD + value: "$(_pod_ns):$(_pod_name)" +- name: LINKERD2_PROXY_INBOUND_DEFAULT_POLICY + value: {{.Values.proxy.defaultInboundPolicy}} +- name: LINKERD2_PROXY_POLICY_CLUSTER_NETWORKS + value: {{.Values.clusterNetworks | quote}} +{{ if .Values.proxy.inboundConnectTimeout -}} +- name: LINKERD2_PROXY_INBOUND_CONNECT_TIMEOUT + value: {{.Values.proxy.inboundConnectTimeout | quote}} +{{ end -}} +{{ if .Values.proxy.outboundConnectTimeout -}} +- name: LINKERD2_PROXY_OUTBOUND_CONNECT_TIMEOUT + value: {{.Values.proxy.outboundConnectTimeout | quote}} +{{ end -}} +{{ if .Values.proxy.outboundDiscoveryCacheUnusedTimeout -}} +- name: LINKERD2_PROXY_OUTBOUND_DISCOVERY_IDLE_TIMEOUT + value: {{.Values.proxy.outboundDiscoveryCacheUnusedTimeout | quote}} +{{ end -}} +{{ if .Values.proxy.inboundDiscoveryCacheUnusedTimeout -}} +- name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT + value: {{.Values.proxy.inboundDiscoveryCacheUnusedTimeout | quote}} +{{ end -}} +{{ if .Values.proxy.disableOutboundProtocolDetectTimeout -}} +- name: LINKERD2_PROXY_OUTBOUND_DETECT_TIMEOUT + value: "365d" +{{ end -}} +{{ if .Values.proxy.disableInboundProtocolDetectTimeout -}} +- name: LINKERD2_PROXY_INBOUND_DETECT_TIMEOUT + value: "365d" +{{ end -}} +- name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR + value: 0.0.0.0:{{.Values.proxy.ports.control}} +- name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR + value: 0.0.0.0:{{.Values.proxy.ports.admin}} +- name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR + value: 127.0.0.1:{{.Values.proxy.ports.outbound}} +- name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR + value: 0.0.0.0:{{.Values.proxy.ports.inbound}} +- name: LINKERD2_PROXY_INBOUND_IPS + valueFrom: + fieldRef: + fieldPath: status.podIPs +- name: LINKERD2_PROXY_INBOUND_PORTS + value: {{ .Values.proxy.podInboundPorts | quote }} +{{ if .Values.proxy.isGateway -}} +- name: LINKERD2_PROXY_INBOUND_GATEWAY_SUFFIXES + value: {{printf "svc.%s." .Values.clusterDomain}} +{{ end -}} +{{ if .Values.proxy.isIngress -}} +- name: LINKERD2_PROXY_INGRESS_MODE + value: "true" +{{ end -}} +- name: LINKERD2_PROXY_DESTINATION_PROFILE_SUFFIXES + {{- $internalDomain := printf "svc.%s." .Values.clusterDomain }} + value: {{ternary "." $internalDomain .Values.proxy.enableExternalProfiles}} +- name: LINKERD2_PROXY_INBOUND_ACCEPT_KEEPALIVE + value: 10000ms +- name: LINKERD2_PROXY_OUTBOUND_CONNECT_KEEPALIVE + value: 10000ms +{{ if .Values.proxy.opaquePorts -}} +- name: LINKERD2_PROXY_INBOUND_PORTS_DISABLE_PROTOCOL_DETECTION + value: {{.Values.proxy.opaquePorts | quote}} +{{ end -}} +- name: LINKERD2_PROXY_DESTINATION_CONTEXT + value: | + {"ns":"$(_pod_ns)", "nodeName":"$(_pod_nodeName)", "pod":"$(_pod_name)"} +- name: _pod_sa + valueFrom: + fieldRef: + fieldPath: spec.serviceAccountName +- name: _l5d_ns + value: {{.Release.Namespace}} +- name: _l5d_trustdomain + value: {{$trustDomain}} +- name: LINKERD2_PROXY_IDENTITY_DIR + value: /var/run/linkerd/identity/end-entity +- name: LINKERD2_PROXY_IDENTITY_TRUST_ANCHORS +{{- /* +Pods in the `linkerd` namespace are not injected by the proxy injector and instead obtain +the trust anchor bundle from the `linkerd-identity-trust-roots` configmap. This should not +be used in other contexts. +*/}} +{{- if .Values.proxy.loadTrustBundleFromConfigMap }} + valueFrom: + configMapKeyRef: + name: linkerd-identity-trust-roots + key: ca-bundle.crt +{{ else }} + value: | + {{- required "Please provide the identity trust anchors" .Values.identityTrustAnchorsPEM | trim | nindent 4 }} +{{ end -}} +- name: LINKERD2_PROXY_IDENTITY_TOKEN_FILE +{{- if .Values.identity.serviceAccountTokenProjection }} + value: /var/run/secrets/tokens/linkerd-identity-token +{{ else }} + value: /var/run/secrets/kubernetes.io/serviceaccount/token +{{ end -}} +- name: LINKERD2_PROXY_IDENTITY_SVC_ADDR + value: {{ternary "localhost.:8080" (printf "linkerd-identity-headless.%s.svc.%s.:8080" .Release.Namespace .Values.clusterDomain) (eq (toString .Values.proxy.component) "linkerd-identity")}} +- name: LINKERD2_PROXY_IDENTITY_LOCAL_NAME + value: $(_pod_sa).$(_pod_ns).serviceaccount.identity.{{.Release.Namespace}}.{{$trustDomain}} +- name: LINKERD2_PROXY_IDENTITY_SVC_NAME + value: linkerd-identity.{{.Release.Namespace}}.serviceaccount.identity.{{.Release.Namespace}}.{{$trustDomain}} +- name: LINKERD2_PROXY_DESTINATION_SVC_NAME + value: linkerd-destination.{{.Release.Namespace}}.serviceaccount.identity.{{.Release.Namespace}}.{{$trustDomain}} +- name: LINKERD2_PROXY_POLICY_SVC_NAME + value: linkerd-destination.{{.Release.Namespace}}.serviceaccount.identity.{{.Release.Namespace}}.{{$trustDomain}} +{{ if .Values.proxy.accessLog -}} +- name: LINKERD2_PROXY_ACCESS_LOG + value: {{.Values.proxy.accessLog | quote}} +{{ end -}} +{{ if .Values.proxy.shutdownGracePeriod -}} +- name: LINKERD2_PROXY_SHUTDOWN_GRACE_PERIOD + value: {{.Values.proxy.shutdownGracePeriod | quote}} +{{ end -}} +image: {{.Values.proxy.image.name}}:{{.Values.proxy.image.version | default .Values.linkerdVersion}} +imagePullPolicy: {{.Values.proxy.image.pullPolicy | default .Values.imagePullPolicy}} +livenessProbe: + httpGet: + path: /live + port: {{.Values.proxy.ports.admin}} + initialDelaySeconds: 10 +name: linkerd-proxy +ports: +- containerPort: {{.Values.proxy.ports.inbound}} + name: linkerd-proxy +- containerPort: {{.Values.proxy.ports.admin}} + name: linkerd-admin +readinessProbe: + httpGet: + path: /ready + port: {{.Values.proxy.ports.admin}} + initialDelaySeconds: 2 +{{- if .Values.proxy.resources }} +{{ include "partials.resources" .Values.proxy.resources }} +{{- end }} +securityContext: + allowPrivilegeEscalation: false + {{- if .Values.proxy.capabilities -}} + {{- include "partials.proxy.capabilities" . | nindent 2 -}} + {{- end }} + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: {{.Values.proxy.uid}} + seccompProfile: + type: RuntimeDefault +terminationMessagePolicy: FallbackToLogsOnError +{{- if or (.Values.proxy.await) (.Values.proxy.waitBeforeExitSeconds) }} +lifecycle: +{{- if .Values.proxy.await }} + postStart: + exec: + command: + - /usr/lib/linkerd/linkerd-await + - --timeout=2m + - --port={{.Values.proxy.ports.admin}} +{{- end }} +{{- if .Values.proxy.waitBeforeExitSeconds }} + preStop: + exec: + command: + - /bin/sleep + - {{.Values.proxy.waitBeforeExitSeconds | quote}} +{{- end }} +{{- end }} +volumeMounts: +- mountPath: /var/run/linkerd/identity/end-entity + name: linkerd-identity-end-entity +{{- if .Values.identity.serviceAccountTokenProjection }} +- mountPath: /var/run/secrets/tokens + name: linkerd-identity-token +{{- end }} +{{- if .Values.proxy.saMountPath }} +- mountPath: {{.Values.proxy.saMountPath.mountPath}} + name: {{.Values.proxy.saMountPath.name}} + readOnly: {{.Values.proxy.saMountPath.readOnly}} +{{- end -}} +{{- end }} diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/templates/_pull-secrets.tpl b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_pull-secrets.tpl new file mode 100644 index 0000000..0c9aa4f --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_pull-secrets.tpl @@ -0,0 +1,6 @@ +{{- define "partials.image-pull-secrets"}} +{{- if . }} +imagePullSecrets: +{{ toYaml . | indent 2 }} +{{- end }} +{{- end -}} diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/templates/_resources.tpl b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_resources.tpl new file mode 100644 index 0000000..1fd6789 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_resources.tpl @@ -0,0 +1,28 @@ +{{- define "partials.resources" -}} +{{- $ephemeralStorage := index . "ephemeral-storage" -}} +resources: + {{- if or (.cpu).limit (.memory).limit ($ephemeralStorage).limit }} + limits: + {{- with (.cpu).limit }} + cpu: {{. | quote}} + {{- end }} + {{- with (.memory).limit }} + memory: {{. | quote}} + {{- end }} + {{- with ($ephemeralStorage).limit }} + ephemeral-storage: {{. | quote}} + {{- end }} + {{- end }} + {{- if or (.cpu).request (.memory).request ($ephemeralStorage).request }} + requests: + {{- with (.cpu).request }} + cpu: {{. | quote}} + {{- end }} + {{- with (.memory).request }} + memory: {{. | quote}} + {{- end }} + {{- with ($ephemeralStorage).request }} + ephemeral-storage: {{. | quote}} + {{- end }} + {{- end }} +{{- end }} diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/templates/_tolerations.tpl b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_tolerations.tpl new file mode 100644 index 0000000..c2292b1 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_tolerations.tpl @@ -0,0 +1,4 @@ +{{- define "linkerd.tolerations" -}} +tolerations: +{{ toYaml .Values.tolerations | trim | indent 2 }} +{{- end -}} diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/templates/_trace.tpl b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_trace.tpl new file mode 100644 index 0000000..dee0595 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_trace.tpl @@ -0,0 +1,5 @@ +{{ define "partials.linkerd.trace" -}} +{{ if .Values.controlPlaneTracing -}} +- -trace-collector=collector.{{.Values.controlPlaneTracingNamespace}}.svc.{{.Values.clusterDomain}}:55678 +{{ end -}} +{{- end }} diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/templates/_validate.tpl b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_validate.tpl new file mode 100644 index 0000000..ba772c2 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_validate.tpl @@ -0,0 +1,19 @@ +{{- define "linkerd.webhook.validation" -}} + +{{- if and (.injectCaFrom) (.injectCaFromSecret) -}} +{{- fail "injectCaFrom and injectCaFromSecret cannot both be set" -}} +{{- end -}} + +{{- if and (or (.injectCaFrom) (.injectCaFromSecret)) (.caBundle) -}} +{{- fail "injectCaFrom or injectCaFromSecret cannot be set if providing a caBundle" -}} +{{- end -}} + +{{- if and (.externalSecret) (empty .caBundle) (empty .injectCaFrom) (empty .injectCaFromSecret) -}} +{{- fail "if externalSecret is set, then caBundle, injectCaFrom, or injectCaFromSecret must be set" -}} +{{- end }} + +{{- if and (or .injectCaFrom .injectCaFromSecret .caBundle) (not .externalSecret) -}} +{{- fail "if caBundle, injectCaFrom, or injectCaFromSecret is set, then externalSecret must be set" -}} +{{- end -}} + +{{- end -}} diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/templates/_volumes.tpl b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_volumes.tpl new file mode 100644 index 0000000..9684cf2 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/charts/partials/templates/_volumes.tpl @@ -0,0 +1,20 @@ +{{ define "partials.proxy.volumes.identity" -}} +emptyDir: + medium: Memory +name: linkerd-identity-end-entity +{{- end -}} + +{{ define "partials.proxyInit.volumes.xtables" -}} +emptyDir: {} +name: {{ .Values.proxyInit.xtMountPath.name }} +{{- end -}} + +{{- define "partials.proxy.volumes.service-account-token" -}} +name: linkerd-identity-token +projected: + sources: + - serviceAccountToken: + path: linkerd-identity-token + expirationSeconds: 86400 {{- /* # 24 hours */}} + audience: identity.l5d.io +{{- end -}} diff --git a/linkerd/charts/linkerd-control-plane/charts/partials/values.yaml b/linkerd/charts/linkerd-control-plane/charts/partials/values.yaml new file mode 100644 index 0000000..e69de29 diff --git a/linkerd/charts/linkerd-control-plane/templates/NOTES.txt b/linkerd/charts/linkerd-control-plane/templates/NOTES.txt new file mode 100644 index 0000000..4bd1be9 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/templates/NOTES.txt @@ -0,0 +1,19 @@ +The Linkerd control plane was successfully installed 🎉 + +To help you manage your Linkerd service mesh you can install the Linkerd CLI by running: + + curl -sL https://run.linkerd.io/install | sh + +Alternatively, you can download the CLI directly via the Linkerd releases page: + + https://github.com/linkerd/linkerd2/releases/ + +To make sure everything works as expected, run the following: + + linkerd check + +The viz extension can be installed by running: + + helm install linkerd-viz linkerd/linkerd-viz + +Looking for more? Visit https://linkerd.io/2/getting-started/ diff --git a/linkerd/charts/linkerd-control-plane/templates/config-rbac.yaml b/linkerd/charts/linkerd-control-plane/templates/config-rbac.yaml new file mode 100644 index 0000000..b55efbd --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/templates/config-rbac.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + {{- with .Values.commonLabels }} + labels: {{ toYaml . | trim | nindent 4 }} + {{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} + name: ext-namespace-metadata-linkerd-config + namespace: {{ .Release.Namespace }} +rules: +- apiGroups: [""] + resources: ["configmaps"] + verbs: ["get"] + resourceNames: ["linkerd-config"] diff --git a/linkerd/charts/linkerd-control-plane/templates/config.yaml b/linkerd/charts/linkerd-control-plane/templates/config.yaml new file mode 100644 index 0000000..a9cea5f --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/templates/config.yaml @@ -0,0 +1,39 @@ +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: linkerd-config + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: controller + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +data: + linkerd-crds-chart-version: linkerd-crds-1.0.0-edge + values: | + {{- $values := deepCopy .Values }} + {{- /* + WARNING! All sensitive or private data such as TLS keys must be removed + here to avoid it being publicly readable. + */ -}} + {{- if kindIs "map" $values.identity.issuer.tls -}} + {{- $_ := unset $values.identity.issuer.tls "keyPEM"}} + {{- end -}} + {{- if kindIs "map" $values.profileValidator -}} + {{- $_ := unset $values.profileValidator "keyPEM"}} + {{- end -}} + {{- if kindIs "map" $values.proxyInjector -}} + {{- $_ := unset $values.proxyInjector "keyPEM"}} + {{- end -}} + {{- if kindIs "map" $values.policyValidator -}} + {{- $_ := unset $values.policyValidator "keyPEM"}} + {{- end -}} + {{- if (empty $values.identityTrustDomain) -}} + {{- $_ := set $values "identityTrustDomain" $values.clusterDomain}} + {{- end -}} + {{- $_ := unset $values "partials"}} + {{- $_ := unset $values "configs"}} + {{- $_ := unset $values "stage"}} + {{- toYaml $values | trim | nindent 4 }} diff --git a/linkerd/charts/linkerd-control-plane/templates/destination-rbac.yaml b/linkerd/charts/linkerd-control-plane/templates/destination-rbac.yaml new file mode 100644 index 0000000..840be62 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/templates/destination-rbac.yaml @@ -0,0 +1,305 @@ +--- +### +### Destination Controller Service +### +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: linkerd-{{.Release.Namespace}}-destination + labels: + linkerd.io/control-plane-component: destination + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +rules: +- apiGroups: ["apps"] + resources: ["replicasets"] + verbs: ["list", "get", "watch"] +- apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["list", "get", "watch"] +- apiGroups: [""] + resources: ["pods", "endpoints", "services", "nodes"] + verbs: ["list", "get", "watch"] +- apiGroups: ["linkerd.io"] + resources: ["serviceprofiles"] + verbs: ["list", "get", "watch"] + {{- if .Values.enableEndpointSlices }} +- apiGroups: ["discovery.k8s.io"] + resources: ["endpointslices"] + verbs: ["list", "get", "watch"] + {{- end }} +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: linkerd-{{.Release.Namespace}}-destination + labels: + linkerd.io/control-plane-component: destination + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: linkerd-{{.Release.Namespace}}-destination +subjects: +- kind: ServiceAccount + name: linkerd-destination + namespace: {{.Release.Namespace}} +--- +kind: ServiceAccount +apiVersion: v1 +metadata: + name: linkerd-destination + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: destination + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +{{- include "partials.image-pull-secrets" .Values.imagePullSecrets }} +--- +{{- $host := printf "linkerd-sp-validator.%s.svc" .Release.Namespace }} +{{- $ca := genSelfSignedCert $host (list) (list $host) 365 }} +{{- if (not .Values.profileValidator.externalSecret) }} +kind: Secret +apiVersion: v1 +metadata: + name: linkerd-sp-validator-k8s-tls + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: destination + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +type: kubernetes.io/tls +data: + tls.crt: {{ ternary (b64enc (trim $ca.Cert)) (b64enc (trim .Values.profileValidator.crtPEM)) (empty .Values.profileValidator.crtPEM) }} + tls.key: {{ ternary (b64enc (trim $ca.Key)) (b64enc (trim .Values.profileValidator.keyPEM)) (empty .Values.profileValidator.keyPEM) }} +--- +{{- end }} +{{- include "linkerd.webhook.validation" .Values.profileValidator }} +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: linkerd-sp-validator-webhook-config + {{- if or (.Values.profileValidator.injectCaFrom) (.Values.profileValidator.injectCaFromSecret) }} + annotations: + {{- if .Values.profileValidator.injectCaFrom }} + cert-manager.io/inject-ca-from: {{ .Values.profileValidator.injectCaFrom }} + {{- end }} + {{- if .Values.profileValidator.injectCaFromSecret }} + cert-manager.io/inject-ca-from-secret: {{ .Values.profileValidator.injectCaFromSecret }} + {{- end }} + {{- end }} + labels: + linkerd.io/control-plane-component: destination + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +webhooks: +- name: linkerd-sp-validator.linkerd.io + namespaceSelector: + {{- toYaml .Values.profileValidator.namespaceSelector | trim | nindent 4 }} + clientConfig: + service: + name: linkerd-sp-validator + namespace: {{ .Release.Namespace }} + path: "/" + {{- if and (empty .Values.profileValidator.injectCaFrom) (empty .Values.profileValidator.injectCaFromSecret) }} + caBundle: {{ ternary (b64enc (trim $ca.Cert)) (b64enc (trim .Values.profileValidator.caBundle)) (empty .Values.profileValidator.caBundle) }} + {{- end }} + failurePolicy: {{.Values.webhookFailurePolicy}} + admissionReviewVersions: ["v1", "v1beta1"] + rules: + - operations: ["CREATE", "UPDATE"] + apiGroups: ["linkerd.io"] + apiVersions: ["v1alpha1", "v1alpha2"] + resources: ["serviceprofiles"] + sideEffects: None +--- +{{- $host := printf "linkerd-policy-validator.%s.svc" .Release.Namespace }} +{{- $ca := genSelfSignedCert $host (list) (list $host) 365 }} +{{- if (not .Values.policyValidator.externalSecret) }} +kind: Secret +apiVersion: v1 +metadata: + name: linkerd-policy-validator-k8s-tls + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: destination + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +type: kubernetes.io/tls +data: + tls.crt: {{ ternary (b64enc (trim $ca.Cert)) (b64enc (trim .Values.policyValidator.crtPEM)) (empty .Values.policyValidator.crtPEM) }} + tls.key: {{ ternary (b64enc (trim $ca.Key)) (b64enc (trim .Values.policyValidator.keyPEM)) (empty .Values.policyValidator.keyPEM) }} +--- +{{- end }} +{{- include "linkerd.webhook.validation" .Values.policyValidator }} +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: linkerd-policy-validator-webhook-config + {{- if or (.Values.policyValidator.injectCaFrom) (.Values.policyValidator.injectCaFromSecret) }} + annotations: + {{- if .Values.policyValidator.injectCaFrom }} + cert-manager.io/inject-ca-from: {{ .Values.policyValidator.injectCaFrom }} + {{- end }} + {{- if .Values.policyValidator.injectCaFromSecret }} + cert-manager.io/inject-ca-from-secret: {{ .Values.policyValidator.injectCaFromSecret }} + {{- end }} + {{- end }} + labels: + linkerd.io/control-plane-component: destination + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +webhooks: +- name: linkerd-policy-validator.linkerd.io + namespaceSelector: + {{- toYaml .Values.policyValidator.namespaceSelector | trim | nindent 4 }} + clientConfig: + service: + name: linkerd-policy-validator + namespace: {{ .Release.Namespace }} + path: "/" + {{- if and (empty .Values.policyValidator.injectCaFrom) (empty .Values.policyValidator.injectCaFromSecret) }} + caBundle: {{ ternary (b64enc (trim $ca.Cert)) (b64enc (trim .Values.policyValidator.caBundle)) (empty .Values.policyValidator.caBundle) }} + {{- end }} + failurePolicy: {{.Values.webhookFailurePolicy}} + admissionReviewVersions: ["v1", "v1beta1"] + rules: + - operations: ["CREATE", "UPDATE"] + apiGroups: ["policy.linkerd.io"] + apiVersions: ["*"] + resources: + - authorizationpolicies + - httproutes + - networkauthentications + - meshtlsauthentications + - serverauthorizations + - servers + sideEffects: None +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: linkerd-policy + labels: + app.kubernetes.io/part-of: Linkerd + linkerd.io/control-plane-component: destination + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +rules: + - apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - get + - apiGroups: + - policy.linkerd.io + resources: + - authorizationpolicies + - httproutes + - meshtlsauthentications + - networkauthentications + - servers + - serverauthorizations + verbs: + - get + - list + - watch + - apiGroups: + - gateway.networking.k8s.io + resources: + - httproutes + verbs: + - get + - list + - watch + - apiGroups: + - policy.linkerd.io + resources: + - httproutes/status + verbs: + - patch + - apiGroups: + - gateway.networking.k8s.io + resources: + - httproutes/status + verbs: + - patch + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - get + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: linkerd-destination-policy + labels: + app.kubernetes.io/part-of: Linkerd + linkerd.io/control-plane-component: destination + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: linkerd-policy +subjects: + - kind: ServiceAccount + name: linkerd-destination + namespace: {{.Release.Namespace}} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: remote-discovery + namespace: {{.Release.Namespace}} + labels: + app.kubernetes.io/part-of: Linkerd + linkerd.io/control-plane-component: destination + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +rules: + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: linkerd-destination-remote-discovery + namespace: {{.Release.Namespace}} + labels: + app.kubernetes.io/part-of: Linkerd + linkerd.io/control-plane-component: destination + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: remote-discovery +subjects: + - kind: ServiceAccount + name: linkerd-destination + namespace: {{.Release.Namespace}} diff --git a/linkerd/charts/linkerd-control-plane/templates/destination.yaml b/linkerd/charts/linkerd-control-plane/templates/destination.yaml new file mode 100644 index 0000000..d999274 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/templates/destination.yaml @@ -0,0 +1,364 @@ +--- +### +### Destination Controller Service +### +kind: Service +apiVersion: v1 +metadata: + name: linkerd-dst + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: destination + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + type: ClusterIP + selector: + linkerd.io/control-plane-component: destination + ports: + - name: grpc + port: 8086 + targetPort: 8086 +--- +kind: Service +apiVersion: v1 +metadata: + name: linkerd-dst-headless + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: destination + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + clusterIP: None + selector: + linkerd.io/control-plane-component: destination + ports: + - name: grpc + port: 8086 + targetPort: 8086 +--- +kind: Service +apiVersion: v1 +metadata: + name: linkerd-sp-validator + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: destination + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + type: ClusterIP + selector: + linkerd.io/control-plane-component: destination + ports: + - name: sp-validator + port: 443 + targetPort: sp-validator +--- +kind: Service +apiVersion: v1 +metadata: + name: linkerd-policy + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: destination + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + clusterIP: None + selector: + linkerd.io/control-plane-component: destination + ports: + - name: grpc + port: 8090 + targetPort: 8090 +--- +kind: Service +apiVersion: v1 +metadata: + name: linkerd-policy-validator + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: destination + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + type: ClusterIP + selector: + linkerd.io/control-plane-component: destination + ports: + - name: policy-https + port: 443 + targetPort: policy-https +{{- if .Values.enablePodDisruptionBudget }} +--- +kind: PodDisruptionBudget +apiVersion: policy/v1 +metadata: + name: linkerd-dst + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: destination + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + maxUnavailable: 1 + selector: + matchLabels: + linkerd.io/control-plane-component: destination +{{- end }} +--- +{{- $tree := deepCopy . }} +{{ $_ := set $tree.Values.proxy "workloadKind" "deployment" -}} +{{ $_ := set $tree.Values.proxy "component" "linkerd-destination" -}} +{{ $_ := set $tree.Values.proxy "waitBeforeExitSeconds" 0 -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + app.kubernetes.io/name: destination + app.kubernetes.io/part-of: Linkerd + app.kubernetes.io/version: {{.Values.linkerdVersion}} + linkerd.io/control-plane-component: destination + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + name: linkerd-destination + namespace: {{ .Release.Namespace }} +spec: + replicas: {{.Values.controllerReplicas}} + selector: + matchLabels: + linkerd.io/control-plane-component: destination + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- include "partials.proxy.labels" $tree.Values.proxy | nindent 6}} + {{- if .Values.deploymentStrategy }} + strategy: + {{- with .Values.deploymentStrategy }}{{ toYaml . | trim | nindent 4 }}{{- end }} + {{- end }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/destination-rbac.yaml") . | sha256sum }} + {{ include "partials.annotations.created-by" . }} + {{- include "partials.proxy.annotations" . | nindent 8}} + {{- with .Values.podAnnotations }}{{ toYaml . | trim | nindent 8 }}{{- end }} + config.linkerd.io/default-inbound-policy: "all-unauthenticated" + labels: + linkerd.io/control-plane-component: destination + linkerd.io/control-plane-ns: {{.Release.Namespace}} + linkerd.io/workload-ns: {{.Release.Namespace}} + {{- include "partials.proxy.labels" $tree.Values.proxy | nindent 8}} + {{- with .Values.podLabels }}{{ toYaml . | trim | nindent 8 }}{{- end }} + spec: + {{- with .Values.runtimeClassName }} + runtimeClassName: {{ . | quote }} + {{- end }} + {{- if .Values.tolerations -}} + {{- include "linkerd.tolerations" . | nindent 6 }} + {{- end -}} + {{- include "linkerd.node-selector" . | nindent 6 }} + {{- $_ := set $tree "component" "destination" -}} + {{- include "linkerd.affinity" $tree | nindent 6 }} + containers: + {{- if not (empty .Values.destinationProxyResources) }} + {{- $r := merge .Values.destinationProxyResources .Values.proxy.resources }} + {{- $_ := set $tree.Values.proxy "resources" $r }} + {{- end }} + {{- $_ := set $tree.Values.proxy "await" $tree.Values.proxy.await }} + {{- $_ := set $tree.Values.proxy "loadTrustBundleFromConfigMap" true }} + {{- $_ := set $tree.Values.proxy "podInboundPorts" "8086,8090,8443,9443,9990,9996,9997" }} + {{- $_ := set $tree.Values.proxy "outboundDiscoveryCacheUnusedTimeout" "5s" }} + {{- $_ := set $tree.Values.proxy "inboundDiscoveryCacheUnusedTimeout" "90s" }} + {{- /* + The pod needs to accept webhook traffic, and we can't rely on that originating in the + cluster network. + */}} + {{- $_ := set $tree.Values.proxy "defaultInboundPolicy" "all-unauthenticated" }} + {{- $_ := set $tree.Values.proxy "capabilities" (dict "drop" (list "ALL")) }} + - {{- include "partials.proxy" $tree | indent 8 | trimPrefix (repeat 7 " ") }} + - args: + - destination + - -addr=:8086 + - -controller-namespace={{.Release.Namespace}} + - -enable-h2-upgrade={{.Values.enableH2Upgrade}} + - -log-level={{.Values.controllerLogLevel}} + - -log-format={{.Values.controllerLogFormat}} + - -enable-endpoint-slices={{.Values.enableEndpointSlices}} + - -cluster-domain={{.Values.clusterDomain}} + - -identity-trust-domain={{.Values.identityTrustDomain | default .Values.clusterDomain}} + - -default-opaque-ports={{.Values.proxy.opaquePorts}} + - -enable-pprof={{.Values.enablePprof | default false}} + {{- include "partials.linkerd.trace" . | nindent 8 -}} + image: {{.Values.controllerImage}}:{{.Values.controllerImageVersion | default .Values.linkerdVersion}} + imagePullPolicy: {{.Values.imagePullPolicy}} + livenessProbe: + httpGet: + path: /ping + port: 9996 + initialDelaySeconds: 10 + name: destination + ports: + - containerPort: 8086 + name: grpc + - containerPort: 9996 + name: admin-http + readinessProbe: + failureThreshold: 7 + httpGet: + path: /ready + port: 9996 + {{- if .Values.destinationResources -}} + {{- include "partials.resources" .Values.destinationResources | nindent 8 }} + {{- end }} + securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: {{.Values.controllerUID}} + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault + - args: + - sp-validator + - -log-level={{.Values.controllerLogLevel}} + - -log-format={{.Values.controllerLogFormat}} + - -enable-pprof={{.Values.enablePprof | default false}} + image: {{.Values.controllerImage}}:{{.Values.controllerImageVersion | default .Values.linkerdVersion}} + imagePullPolicy: {{.Values.imagePullPolicy}} + livenessProbe: + httpGet: + path: /ping + port: 9997 + initialDelaySeconds: 10 + name: sp-validator + ports: + - containerPort: 8443 + name: sp-validator + - containerPort: 9997 + name: admin-http + readinessProbe: + failureThreshold: 7 + httpGet: + path: /ready + port: 9997 + {{- if .Values.spValidatorResources -}} + {{- include "partials.resources" .Values.spValidatorResources | nindent 8 }} + {{- end }} + securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: {{.Values.controllerUID}} + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault + volumeMounts: + - mountPath: /var/run/linkerd/tls + name: sp-tls + readOnly: true + - args: + - --admin-addr=0.0.0.0:9990 + - --control-plane-namespace={{.Release.Namespace}} + - --grpc-addr=0.0.0.0:8090 + - --server-addr=0.0.0.0:9443 + - --server-tls-key=/var/run/linkerd/tls/tls.key + - --server-tls-certs=/var/run/linkerd/tls/tls.crt + - --cluster-networks={{.Values.clusterNetworks}} + - --identity-domain={{.Values.identityTrustDomain | default .Values.clusterDomain}} + - --cluster-domain={{.Values.clusterDomain}} + - --default-policy={{.Values.proxy.defaultInboundPolicy}} + - --log-level={{.Values.policyController.logLevel | default "linkerd=info,warn"}} + - --log-format={{.Values.controllerLogFormat}} + - --default-opaque-ports={{.Values.proxy.opaquePorts}} + {{- if .Values.policyController.probeNetworks }} + - --probe-networks={{.Values.policyController.probeNetworks | join ","}} + {{- end}} + image: {{.Values.policyController.image.name}}:{{.Values.policyController.image.version | default .Values.linkerdVersion}} + imagePullPolicy: {{.Values.policyController.image.pullPolicy | default .Values.imagePullPolicy}} + livenessProbe: + httpGet: + path: /live + port: admin-http + name: policy + ports: + - containerPort: 8090 + name: grpc + - containerPort: 9990 + name: admin-http + - containerPort: 9443 + name: policy-https + readinessProbe: + failureThreshold: 7 + httpGet: + path: /ready + port: admin-http + initialDelaySeconds: 10 + {{- $res := .Values.policyController.resources | default .Values.destinationResources }} + {{- if $res }} + {{- include "partials.resources" $res | nindent 8 }} + {{- end }} + securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: {{.Values.controllerUID}} + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault + volumeMounts: + - mountPath: /var/run/linkerd/tls + name: policy-tls + readOnly: true + initContainers: + {{ if .Values.cniEnabled -}} + - {{- include "partials.network-validator" $tree | indent 8 | trimPrefix (repeat 7 " ") }} + {{ else -}} + {{- /* + The destination controller needs to connect to the Kubernetes API before the proxy is able + to proxy requests, so we always skip these connections. + */}} + {{- $_ := set $tree.Values.proxyInit "ignoreOutboundPorts" .Values.proxyInit.kubeAPIServerPorts -}} + - {{- include "partials.proxy-init" $tree | indent 8 | trimPrefix (repeat 7 " ") }} + {{ end -}} + {{- if .Values.priorityClassName -}} + priorityClassName: {{ .Values.priorityClassName }} + {{ end -}} + securityContext: + seccompProfile: + type: RuntimeDefault + serviceAccountName: linkerd-destination + volumes: + - name: sp-tls + secret: + secretName: linkerd-sp-validator-k8s-tls + - name: policy-tls + secret: + secretName: linkerd-policy-validator-k8s-tls + {{ if not .Values.cniEnabled -}} + - {{- include "partials.proxyInit.volumes.xtables" . | indent 8 | trimPrefix (repeat 7 " ") }} + {{ end -}} + {{if .Values.identity.serviceAccountTokenProjection -}} + - {{- include "partials.proxy.volumes.service-account-token" . | indent 8 | trimPrefix (repeat 7 " ") }} + {{ end -}} + - {{- include "partials.proxy.volumes.identity" . | indent 8 | trimPrefix (repeat 7 " ") }} diff --git a/linkerd/charts/linkerd-control-plane/templates/heartbeat-rbac.yaml b/linkerd/charts/linkerd-control-plane/templates/heartbeat-rbac.yaml new file mode 100644 index 0000000..7b12754 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/templates/heartbeat-rbac.yaml @@ -0,0 +1,78 @@ +{{ if not .Values.disableHeartBeat -}} +--- +### +### Heartbeat RBAC +### +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: linkerd-heartbeat + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +rules: +- apiGroups: [""] + resources: ["configmaps"] + verbs: ["get"] + resourceNames: ["linkerd-config"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: linkerd-heartbeat + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +roleRef: + kind: Role + name: linkerd-heartbeat + apiGroup: rbac.authorization.k8s.io +subjects: +- kind: ServiceAccount + name: linkerd-heartbeat + namespace: {{.Release.Namespace}} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: linkerd-heartbeat + labels: + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +rules: +- apiGroups: [""] + resources: ["namespaces"] + verbs: ["list"] +- apiGroups: ["linkerd.io"] + resources: ["serviceprofiles"] + verbs: ["list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: linkerd-heartbeat + labels: + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +roleRef: + kind: ClusterRole + name: linkerd-heartbeat + apiGroup: rbac.authorization.k8s.io +subjects: +- kind: ServiceAccount + name: linkerd-heartbeat + namespace: {{.Release.Namespace}} +--- +kind: ServiceAccount +apiVersion: v1 +metadata: + name: linkerd-heartbeat + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: heartbeat + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +{{- include "partials.image-pull-secrets" .Values.imagePullSecrets }} +{{- end }} diff --git a/linkerd/charts/linkerd-control-plane/templates/heartbeat.yaml b/linkerd/charts/linkerd-control-plane/templates/heartbeat.yaml new file mode 100644 index 0000000..aa43874 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/templates/heartbeat.yaml @@ -0,0 +1,85 @@ +{{ if not .Values.disableHeartBeat -}} +--- +### +### Heartbeat +### +apiVersion: batch/v1 +kind: CronJob +metadata: + name: linkerd-heartbeat + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: heartbeat + app.kubernetes.io/part-of: Linkerd + app.kubernetes.io/version: {{.Values.linkerdVersion}} + linkerd.io/control-plane-component: heartbeat + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + concurrencyPolicy: Replace + {{ if .Values.heartbeatSchedule -}} + schedule: "{{.Values.heartbeatSchedule}}" + {{ else -}} + schedule: "{{ dateInZone "04 15 * * *" (now | mustDateModify "+10m") "UTC"}}" + {{ end -}} + successfulJobsHistoryLimit: 0 + jobTemplate: + spec: + template: + metadata: + labels: + linkerd.io/control-plane-component: heartbeat + linkerd.io/workload-ns: {{.Release.Namespace}} + {{- with .Values.podLabels }}{{ toYaml . | trim | nindent 12 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} + {{- with .Values.podAnnotations }}{{ toYaml . | trim | nindent 12 }}{{- end }} + spec: + {{- if .Values.priorityClassName }} + priorityClassName: {{ .Values.priorityClassName }} + {{- end -}} + {{- with .Values.runtimeClassName }} + runtimeClassName: {{ . | quote }} + {{- end }} + {{- if .Values.tolerations -}} + {{- include "linkerd.tolerations" . | nindent 10 }} + {{- end -}} + {{- include "linkerd.node-selector" . | nindent 10 }} + securityContext: + seccompProfile: + type: RuntimeDefault + serviceAccountName: linkerd-heartbeat + restartPolicy: Never + containers: + - name: heartbeat + image: {{.Values.controllerImage}}:{{.Values.controllerImageVersion | default .Values.linkerdVersion}} + imagePullPolicy: {{.Values.imagePullPolicy}} + env: + - name: LINKERD_DISABLED + value: "the heartbeat controller does not use the proxy" + args: + - "heartbeat" + - "-controller-namespace={{.Release.Namespace}}" + - "-log-level={{.Values.controllerLogLevel}}" + - "-log-format={{.Values.controllerLogFormat}}" + {{- if .Values.prometheusUrl }} + - "-prometheus-url={{.Values.prometheusUrl}}" + {{- else }} + - "-prometheus-url=http://prometheus.linkerd-viz.svc.{{.Values.clusterDomain}}:9090" + {{- end }} + {{- if .Values.heartbeatResources -}} + {{- include "partials.resources" .Values.heartbeatResources | nindent 12 }} + {{- end }} + securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: {{.Values.controllerUID}} + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault +{{- end }} diff --git a/linkerd/charts/linkerd-control-plane/templates/identity-rbac.yaml b/linkerd/charts/linkerd-control-plane/templates/identity-rbac.yaml new file mode 100644 index 0000000..6efdb4e --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/templates/identity-rbac.yaml @@ -0,0 +1,49 @@ +--- +### +### Identity Controller Service RBAC +### +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: linkerd-{{.Release.Namespace}}-identity + labels: + linkerd.io/control-plane-component: identity + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +rules: +- apiGroups: ["authentication.k8s.io"] + resources: ["tokenreviews"] + verbs: ["create"] +# TODO(ver) Restrict this to the Linkerd namespace. See +# https://github.com/linkerd/linkerd2/issues/9367 +- apiGroups: [""] + resources: ["events"] + verbs: ["create", "patch"] +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: linkerd-{{.Release.Namespace}}-identity + labels: + linkerd.io/control-plane-component: identity + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: linkerd-{{.Release.Namespace}}-identity +subjects: +- kind: ServiceAccount + name: linkerd-identity + namespace: {{.Release.Namespace}} +--- +kind: ServiceAccount +apiVersion: v1 +metadata: + name: linkerd-identity + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: identity + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +{{- include "partials.image-pull-secrets" .Values.imagePullSecrets }} diff --git a/linkerd/charts/linkerd-control-plane/templates/identity.yaml b/linkerd/charts/linkerd-control-plane/templates/identity.yaml new file mode 100644 index 0000000..3964efe --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/templates/identity.yaml @@ -0,0 +1,254 @@ +{{if .Values.identity -}} +--- +### +### Identity Controller Service +### +{{ if and (.Values.identity.issuer) (eq .Values.identity.issuer.scheme "linkerd.io/tls") -}} +--- +kind: Secret +apiVersion: v1 +metadata: + name: linkerd-identity-issuer + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: identity + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +data: + crt.pem: {{b64enc (required "Please provide the identity issuer certificate" .Values.identity.issuer.tls.crtPEM | trim)}} + key.pem: {{b64enc (required "Please provide the identity issue private key" .Values.identity.issuer.tls.keyPEM | trim)}} +{{- end}} +{{ if not (.Values.identity.externalCA) -}} +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: linkerd-identity-trust-roots + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: identity + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +data: + ca-bundle.crt: |-{{.Values.identityTrustAnchorsPEM | trim | nindent 4}} +{{- end}} +--- +kind: Service +apiVersion: v1 +metadata: + name: linkerd-identity + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: identity + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + type: ClusterIP + selector: + linkerd.io/control-plane-component: identity + ports: + - name: grpc + port: 8080 + targetPort: 8080 +--- +kind: Service +apiVersion: v1 +metadata: + name: linkerd-identity-headless + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: identity + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + clusterIP: None + selector: + linkerd.io/control-plane-component: identity + ports: + - name: grpc + port: 8080 + targetPort: 8080 +{{- if .Values.enablePodDisruptionBudget }} +--- +kind: PodDisruptionBudget +apiVersion: policy/v1 +metadata: + name: linkerd-identity + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: identity + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + maxUnavailable: 1 + selector: + matchLabels: + linkerd.io/control-plane-component: identity +{{- end }} +--- +{{- $tree := deepCopy . }} +{{ $_ := set $tree.Values.proxy "workloadKind" "deployment" -}} +{{ $_ := set $tree.Values.proxy "component" "linkerd-identity" -}} +{{ $_ := set $tree.Values.proxy "waitBeforeExitSeconds" 0 -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + app.kubernetes.io/name: identity + app.kubernetes.io/part-of: Linkerd + app.kubernetes.io/version: {{.Values.linkerdVersion}} + linkerd.io/control-plane-component: identity + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + name: linkerd-identity + namespace: {{ .Release.Namespace }} +spec: + replicas: {{.Values.controllerReplicas}} + selector: + matchLabels: + linkerd.io/control-plane-component: identity + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- include "partials.proxy.labels" $tree.Values.proxy | nindent 6}} + {{- if .Values.deploymentStrategy }} + strategy: + {{- with .Values.deploymentStrategy }}{{ toYaml . | trim | nindent 4 }}{{- end }} + {{- end }} + template: + metadata: + annotations: + {{ include "partials.annotations.created-by" . }} + {{- include "partials.proxy.annotations" . | nindent 8}} + {{- with .Values.podAnnotations }}{{ toYaml . | trim | nindent 8 }}{{- end }} + config.linkerd.io/default-inbound-policy: "all-unauthenticated" + labels: + linkerd.io/control-plane-component: identity + linkerd.io/control-plane-ns: {{.Release.Namespace}} + linkerd.io/workload-ns: {{.Release.Namespace}} + {{- include "partials.proxy.labels" $tree.Values.proxy | nindent 8}} + {{- with .Values.podLabels }}{{ toYaml . | trim | nindent 8 }}{{- end }} + spec: + {{- with .Values.runtimeClassName }} + runtimeClassName: {{ . | quote }} + {{- end }} + {{- if .Values.tolerations -}} + {{- include "linkerd.tolerations" . | nindent 6 }} + {{- end -}} + {{- include "linkerd.node-selector" . | nindent 6 }} + {{- $_ := set $tree "component" "identity" -}} + {{- include "linkerd.affinity" $tree | nindent 6 }} + containers: + - args: + - identity + - -log-level={{.Values.controllerLogLevel}} + - -log-format={{.Values.controllerLogFormat}} + - -controller-namespace={{.Release.Namespace}} + - -identity-trust-domain={{.Values.identityTrustDomain | default .Values.clusterDomain}} + - -identity-issuance-lifetime={{.Values.identity.issuer.issuanceLifetime}} + - -identity-clock-skew-allowance={{.Values.identity.issuer.clockSkewAllowance}} + - -identity-scheme={{.Values.identity.issuer.scheme}} + - -enable-pprof={{.Values.enablePprof | default false}} + - -kube-apiclient-qps={{.Values.identity.kubeAPI.clientQPS}} + - -kube-apiclient-burst={{.Values.identity.kubeAPI.clientBurst}} + {{- include "partials.linkerd.trace" . | nindent 8 -}} + env: + - name: LINKERD_DISABLED + value: "linkerd-await cannot block the identity controller" + image: {{.Values.controllerImage}}:{{.Values.controllerImageVersion | default .Values.linkerdVersion}} + imagePullPolicy: {{.Values.imagePullPolicy}} + livenessProbe: + httpGet: + path: /ping + port: 9990 + initialDelaySeconds: 10 + name: identity + ports: + - containerPort: 8080 + name: grpc + - containerPort: 9990 + name: admin-http + readinessProbe: + failureThreshold: 7 + httpGet: + path: /ready + port: 9990 + {{- if .Values.identityResources -}} + {{- include "partials.resources" .Values.identityResources | nindent 8 }} + {{- end }} + securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: {{.Values.controllerUID}} + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault + volumeMounts: + - mountPath: /var/run/linkerd/identity/issuer + name: identity-issuer + - mountPath: /var/run/linkerd/identity/trust-roots/ + name: trust-roots + {{- if not (empty .Values.identityProxyResources) }} + {{- $r := merge .Values.identityProxyResources .Values.proxy.resources }} + {{- $_ := set $tree.Values.proxy "resources" $r }} + {{- end }} + {{- $_ := set $tree.Values.proxy "await" false }} + {{- $_ := set $tree.Values.proxy "loadTrustBundleFromConfigMap" true }} + {{- $_ := set $tree.Values.proxy "podInboundPorts" "8080,9990" }} + {{- /* + The identity controller cannot discover policies, so we configure it with defaults that + enforce TLS on the identity service. + */}} + {{- $_ := set $tree.Values.proxy "defaultInboundPolicy" "all-unauthenticated" }} + {{- $_ := set $tree.Values.proxy "requireTLSOnInboundPorts" "8080" }} + {{- $_ := set $tree.Values.proxy "capabilities" (dict "drop" (list "ALL")) }} + {{- $_ := set $tree.Values.proxy "outboundDiscoveryCacheUnusedTimeout" "5s" }} + {{- $_ := set $tree.Values.proxy "inboundDiscoveryCacheUnusedTimeout" "90s" }} + - {{- include "partials.proxy" $tree | indent 8 | trimPrefix (repeat 7 " ") }} + initContainers: + {{ if .Values.cniEnabled -}} + - {{- include "partials.network-validator" $tree | indent 8 | trimPrefix (repeat 7 " ") }} + {{ else -}} + {{- /* + The identity controller needs to connect to the Kubernetes API before the proxy is able to + proxy requests, so we always skip these connections. The identity controller makes no other + outbound connections (so it's not important to persist any other skip ports here) + */}} + {{- $_ := set $tree.Values.proxyInit "ignoreOutboundPorts" .Values.proxyInit.kubeAPIServerPorts -}} + - {{- include "partials.proxy-init" $tree | indent 8 | trimPrefix (repeat 7 " ") }} + {{ end -}} + {{- if .Values.priorityClassName -}} + priorityClassName: {{ .Values.priorityClassName }} + {{ end -}} + securityContext: + seccompProfile: + type: RuntimeDefault + serviceAccountName: linkerd-identity + volumes: + - name: identity-issuer + secret: + secretName: linkerd-identity-issuer + - configMap: + name: linkerd-identity-trust-roots + name: trust-roots + {{ if not .Values.cniEnabled -}} + - {{- include "partials.proxyInit.volumes.xtables" . | indent 8 | trimPrefix (repeat 7 " ") }} + {{ end -}} + {{if .Values.identity.serviceAccountTokenProjection -}} + - {{- include "partials.proxy.volumes.service-account-token" . | indent 8 | trimPrefix (repeat 7 " ") }} + {{ end -}} + - {{- include "partials.proxy.volumes.identity" . | indent 8 | trimPrefix (repeat 7 " ") }} +{{end -}} diff --git a/linkerd/charts/linkerd-control-plane/templates/namespace.yaml b/linkerd/charts/linkerd-control-plane/templates/namespace.yaml new file mode 100644 index 0000000..61461c1 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/templates/namespace.yaml @@ -0,0 +1,18 @@ +{{- if eq .Release.Service "CLI" -}} +--- +### +### Linkerd Namespace +### +kind: Namespace +apiVersion: v1 +metadata: + name: {{ .Release.Namespace }} + annotations: + linkerd.io/inject: disabled + labels: + linkerd.io/is-control-plane: "true" + config.linkerd.io/admission-webhooks: disabled + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- /* linkerd-init requires extended capabilities and so requires priviledged mode */}} + pod-security.kubernetes.io/enforce: {{ ternary "restricted" "privileged" .Values.cniEnabled }} +{{ end -}} diff --git a/linkerd/charts/linkerd-control-plane/templates/podmonitor.yaml b/linkerd/charts/linkerd-control-plane/templates/podmonitor.yaml new file mode 100644 index 0000000..fd2b5d6 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/templates/podmonitor.yaml @@ -0,0 +1,128 @@ +{{- $podMonitor := .Values.podMonitor -}} +{{- if and $podMonitor.enabled $podMonitor.controller.enabled }} +--- +### +### Prometheus Operator PodMonitor for Linkerd control-plane +### +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: "linkerd-controller" + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-ns: {{ .Release.Namespace }} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + {{- with .Values.podMonitor.labels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + namespaceSelector: {{ tpl .Values.podMonitor.controller.namespaceSelector . | nindent 4 }} + selector: + matchLabels: {} + podMetricsEndpoints: + - interval: {{ $podMonitor.scrapeInterval }} + scrapeTimeout: {{ $podMonitor.scrapeTimeout }} + relabelings: + - sourceLabels: + - __meta_kubernetes_pod_container_port_name + action: keep + regex: admin-http + - sourceLabels: + - __meta_kubernetes_pod_container_name + action: replace + targetLabel: component +{{- end }} +{{- if and $podMonitor.enabled $podMonitor.serviceMirror.enabled }} +--- +### +### Prometheus Operator PodMonitor for Linkerd Service Mirror (multi-cluster) +### +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: "linkerd-service-mirror" + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-ns: {{ .Release.Namespace }} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + {{- with .Values.podMonitor.labels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + namespaceSelector: + any: true + selector: + matchLabels: {} + podMetricsEndpoints: + - interval: {{ $podMonitor.scrapeInterval }} + scrapeTimeout: {{ $podMonitor.scrapeTimeout }} + relabelings: + - sourceLabels: + - __meta_kubernetes_pod_label_linkerd_io_control_plane_component + - __meta_kubernetes_pod_container_port_name + action: keep + regex: linkerd-service-mirror;admin-http$ + - sourceLabels: + - __meta_kubernetes_pod_container_name + action: replace + targetLabel: component +{{- end }} +{{- if and $podMonitor.enabled $podMonitor.proxy.enabled }} +--- +### +### Prometheus Operator PodMonitor Linkerd data-plane +### +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: "linkerd-proxy" + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-ns: {{ .Release.Namespace }} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + {{- with .Values.podMonitor.labels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + namespaceSelector: + any: true + selector: + matchLabels: {} + podMetricsEndpoints: + - interval: {{ $podMonitor.scrapeInterval }} + scrapeTimeout: {{ $podMonitor.scrapeTimeout }} + relabelings: + - sourceLabels: + - __meta_kubernetes_pod_container_name + - __meta_kubernetes_pod_container_port_name + - __meta_kubernetes_pod_label_linkerd_io_control_plane_ns + action: keep + regex: ^linkerd-proxy;linkerd-admin;{{ .Release.Namespace }}$ + - sourceLabels: [ __meta_kubernetes_namespace ] + action: replace + targetLabel: namespace + - sourceLabels: [ __meta_kubernetes_pod_name ] + action: replace + targetLabel: pod + - sourceLabels: [ __meta_kubernetes_pod_label_linkerd_io_proxy_job ] + action: replace + targetLabel: k8s_job + - action: labeldrop + regex: __meta_kubernetes_pod_label_linkerd_io_proxy_job + - action: labelmap + regex: __meta_kubernetes_pod_label_linkerd_io_proxy_(.+) + - action: labeldrop + regex: __meta_kubernetes_pod_label_linkerd_io_proxy_(.+) + - action: labelmap + regex: __meta_kubernetes_pod_label_linkerd_io_(.+) + - action: labelmap + regex: __meta_kubernetes_pod_label_(.+) + replacement: __tmp_pod_label_$1 + - action: labelmap + regex: __tmp_pod_label_linkerd_io_(.+) + replacement: __tmp_pod_label_$1 + - action: labeldrop + regex: __tmp_pod_label_linkerd_io_(.+) + - action: labelmap + regex: __tmp_pod_label_(.+) +{{- end }} diff --git a/linkerd/charts/linkerd-control-plane/templates/proxy-injector-rbac.yaml b/linkerd/charts/linkerd-control-plane/templates/proxy-injector-rbac.yaml new file mode 100644 index 0000000..5b165de --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/templates/proxy-injector-rbac.yaml @@ -0,0 +1,118 @@ +--- +### +### Proxy Injector RBAC +### +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: linkerd-{{.Release.Namespace}}-proxy-injector + labels: + linkerd.io/control-plane-component: proxy-injector + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +rules: +- apiGroups: [""] + resources: ["events"] + verbs: ["create", "patch"] +- apiGroups: [""] + resources: ["namespaces", "replicationcontrollers"] + verbs: ["list", "get", "watch"] +- apiGroups: [""] + resources: ["pods"] + verbs: ["list", "watch"] +- apiGroups: ["extensions", "apps"] + resources: ["deployments", "replicasets", "daemonsets", "statefulsets"] + verbs: ["list", "get", "watch"] +- apiGroups: ["extensions", "batch"] + resources: ["cronjobs", "jobs"] + verbs: ["list", "get", "watch"] +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: linkerd-{{.Release.Namespace}}-proxy-injector + labels: + linkerd.io/control-plane-component: proxy-injector + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +subjects: +- kind: ServiceAccount + name: linkerd-proxy-injector + namespace: {{.Release.Namespace}} + apiGroup: "" +roleRef: + kind: ClusterRole + name: linkerd-{{.Release.Namespace}}-proxy-injector + apiGroup: rbac.authorization.k8s.io +--- +kind: ServiceAccount +apiVersion: v1 +metadata: + name: linkerd-proxy-injector + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: proxy-injector + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +{{- include "partials.image-pull-secrets" .Values.imagePullSecrets }} +--- +{{- $host := printf "linkerd-proxy-injector.%s.svc" .Release.Namespace }} +{{- $ca := genSelfSignedCert $host (list) (list $host) 365 }} +{{- if (not .Values.proxyInjector.externalSecret) }} +kind: Secret +apiVersion: v1 +metadata: + name: linkerd-proxy-injector-k8s-tls + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: proxy-injector + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +type: kubernetes.io/tls +data: + tls.crt: {{ ternary (b64enc (trim $ca.Cert)) (b64enc (trim .Values.proxyInjector.crtPEM)) (empty .Values.proxyInjector.crtPEM) }} + tls.key: {{ ternary (b64enc (trim $ca.Key)) (b64enc (trim .Values.proxyInjector.keyPEM)) (empty .Values.proxyInjector.keyPEM) }} +--- +{{- end }} +{{- include "linkerd.webhook.validation" .Values.proxyInjector }} +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: linkerd-proxy-injector-webhook-config + {{- if or (.Values.proxyInjector.injectCaFrom) (.Values.proxyInjector.injectCaFromSecret) }} + annotations: + {{- if .Values.proxyInjector.injectCaFrom }} + cert-manager.io/inject-ca-from: {{ .Values.proxyInjector.injectCaFrom }} + {{- end }} + {{- if .Values.proxyInjector.injectCaFromSecret }} + cert-manager.io/inject-ca-from-secret: {{ .Values.proxyInjector.injectCaFromSecret }} + {{- end }} + {{- end }} + labels: + linkerd.io/control-plane-component: proxy-injector + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +webhooks: +- name: linkerd-proxy-injector.linkerd.io + namespaceSelector: + {{- toYaml .Values.proxyInjector.namespaceSelector | trim | nindent 4 }} + objectSelector: + {{- toYaml .Values.proxyInjector.objectSelector | trim | nindent 4 }} + clientConfig: + service: + name: linkerd-proxy-injector + namespace: {{ .Release.Namespace }} + path: "/" + {{- if and (empty .Values.proxyInjector.injectCaFrom) (empty .Values.proxyInjector.injectCaFromSecret) }} + caBundle: {{ ternary (b64enc (trim $ca.Cert)) (b64enc (trim .Values.proxyInjector.caBundle)) (empty .Values.proxyInjector.caBundle) }} + {{- end }} + failurePolicy: {{.Values.webhookFailurePolicy}} + admissionReviewVersions: ["v1", "v1beta1"] + rules: + - operations: [ "CREATE" ] + apiGroups: [""] + apiVersions: ["v1"] + resources: ["pods", "services"] + sideEffects: None diff --git a/linkerd/charts/linkerd-control-plane/templates/proxy-injector.yaml b/linkerd/charts/linkerd-control-plane/templates/proxy-injector.yaml new file mode 100644 index 0000000..89798c0 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/templates/proxy-injector.yaml @@ -0,0 +1,193 @@ +--- +### +### Proxy Injector +### +{{- $tree := deepCopy . }} +{{ $_ := set $tree.Values.proxy "workloadKind" "deployment" -}} +{{ $_ := set $tree.Values.proxy "component" "linkerd-proxy-injector" -}} +{{ $_ := set $tree.Values.proxy "waitBeforeExitSeconds" 0 -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + app.kubernetes.io/name: proxy-injector + app.kubernetes.io/part-of: Linkerd + app.kubernetes.io/version: {{.Values.linkerdVersion}} + linkerd.io/control-plane-component: proxy-injector + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + name: linkerd-proxy-injector + namespace: {{ .Release.Namespace }} +spec: + replicas: {{.Values.controllerReplicas}} + selector: + matchLabels: + linkerd.io/control-plane-component: proxy-injector + {{- if .Values.deploymentStrategy }} + strategy: + {{- with .Values.deploymentStrategy }}{{ toYaml . | trim | nindent 4 }}{{- end }} + {{- end }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/proxy-injector-rbac.yaml") . | sha256sum }} + {{ include "partials.annotations.created-by" . }} + {{- include "partials.proxy.annotations" . | nindent 8}} + {{- with .Values.podAnnotations }}{{ toYaml . | trim | nindent 8 }}{{- end }} + config.linkerd.io/opaque-ports: "8443" + config.linkerd.io/default-inbound-policy: "all-unauthenticated" + labels: + linkerd.io/control-plane-component: proxy-injector + linkerd.io/control-plane-ns: {{.Release.Namespace}} + linkerd.io/workload-ns: {{.Release.Namespace}} + {{- include "partials.proxy.labels" $tree.Values.proxy | nindent 8}} + {{- with .Values.podLabels }}{{ toYaml . | trim | nindent 8 }}{{- end }} + spec: + {{- with .Values.runtimeClassName }} + runtimeClassName: {{ . | quote }} + {{- end }} + {{- if .Values.tolerations -}} + {{- include "linkerd.tolerations" . | nindent 6 }} + {{- end -}} + {{- include "linkerd.node-selector" . | nindent 6 }} + {{- $_ := set $tree "component" "proxy-injector" -}} + {{- include "linkerd.affinity" $tree | nindent 6 }} + containers: + {{- if not (empty .Values.proxyInjectorProxyResources) }} + {{- $r := merge .Values.proxyInjectorProxyResources .Values.proxy.resources }} + {{- $_ := set $tree.Values.proxy "resources" $r }} + {{- end }} + {{- $_ := set $tree.Values.proxy "await" $tree.Values.proxy.await }} + {{- $_ := set $tree.Values.proxy "loadTrustBundleFromConfigMap" true }} + {{- $_ := set $tree.Values.proxy "podInboundPorts" "8443,9995" }} + {{- /* + The pod needs to accept webhook traffic, and we can't rely on that originating in the + cluster network. + */}} + {{- $_ := set $tree.Values.proxy "defaultInboundPolicy" "all-unauthenticated" }} + {{- $_ := set $tree.Values.proxy "capabilities" (dict "drop" (list "ALL")) }} + {{- $_ := set $tree.Values.proxy "outboundDiscoveryCacheUnusedTimeout" "5s" }} + {{- $_ := set $tree.Values.proxy "inboundDiscoveryCacheUnusedTimeout" "90s" }} + - {{- include "partials.proxy" $tree | indent 8 | trimPrefix (repeat 7 " ") }} + - args: + - proxy-injector + - -log-level={{.Values.controllerLogLevel}} + - -log-format={{.Values.controllerLogFormat}} + - -linkerd-namespace={{.Release.Namespace}} + - -enable-pprof={{.Values.enablePprof | default false}} + image: {{.Values.controllerImage}}:{{.Values.controllerImageVersion | default .Values.linkerdVersion}} + imagePullPolicy: {{.Values.imagePullPolicy}} + livenessProbe: + httpGet: + path: /ping + port: 9995 + initialDelaySeconds: 10 + name: proxy-injector + ports: + - containerPort: 8443 + name: proxy-injector + - containerPort: 9995 + name: admin-http + readinessProbe: + failureThreshold: 7 + httpGet: + path: /ready + port: 9995 + {{- if .Values.proxyInjectorResources -}} + {{- include "partials.resources" .Values.proxyInjectorResources | nindent 8 }} + {{- end }} + securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: {{.Values.controllerUID}} + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault + volumeMounts: + - mountPath: /var/run/linkerd/config + name: config + - mountPath: /var/run/linkerd/identity/trust-roots + name: trust-roots + - mountPath: /var/run/linkerd/tls + name: tls + readOnly: true + initContainers: + {{ if .Values.cniEnabled -}} + - {{- include "partials.network-validator" $tree | indent 8 | trimPrefix (repeat 7 " ") }} + {{ else -}} + {{- /* + The controller needs to connect to the Kubernetes API. There's no reason + to put the proxy in the way of that. + */}} + {{- $_ := set $tree.Values.proxyInit "ignoreOutboundPorts" .Values.proxyInit.kubeAPIServerPorts -}} + - {{- include "partials.proxy-init" $tree | indent 8 | trimPrefix (repeat 7 " ") }} + {{ end -}} + {{- if .Values.priorityClassName -}} + priorityClassName: {{ .Values.priorityClassName }} + {{ end -}} + securityContext: + seccompProfile: + type: RuntimeDefault + serviceAccountName: linkerd-proxy-injector + volumes: + - configMap: + name: linkerd-config + name: config + - configMap: + name: linkerd-identity-trust-roots + name: trust-roots + - name: tls + secret: + secretName: linkerd-proxy-injector-k8s-tls + {{ if not .Values.cniEnabled -}} + - {{- include "partials.proxyInit.volumes.xtables" . | indent 8 | trimPrefix (repeat 7 " ") }} + {{ end -}} + {{if .Values.identity.serviceAccountTokenProjection -}} + - {{- include "partials.proxy.volumes.service-account-token" . | indent 8 | trimPrefix (repeat 7 " ") }} + {{ end -}} + - {{- include "partials.proxy.volumes.identity" . | indent 8 | trimPrefix (repeat 7 " ") }} +--- +kind: Service +apiVersion: v1 +metadata: + name: linkerd-proxy-injector + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: proxy-injector + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} + config.linkerd.io/opaque-ports: "443" +spec: + type: ClusterIP + selector: + linkerd.io/control-plane-component: proxy-injector + ports: + - name: proxy-injector + port: 443 + targetPort: proxy-injector +{{- if .Values.enablePodDisruptionBudget }} +--- +kind: PodDisruptionBudget +apiVersion: policy/v1 +metadata: + name: linkerd-proxy-injector + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-component: proxy-injector + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + maxUnavailable: 1 + selector: + matchLabels: + linkerd.io/control-plane-component: proxy-injector +{{- end }} diff --git a/linkerd/charts/linkerd-control-plane/templates/psp.yaml b/linkerd/charts/linkerd-control-plane/templates/psp.yaml new file mode 100644 index 0000000..23cb939 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/templates/psp.yaml @@ -0,0 +1,110 @@ +{{ if .Values.enablePSP -}} +--- +### +### Control Plane PSP +### +apiVersion: policy/v1beta1 +kind: PodSecurityPolicy +metadata: + name: linkerd-{{.Release.Namespace}}-control-plane + annotations: + seccomp.security.alpha.kubernetes.io/allowedProfileNames: "runtime/default" + labels: + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +spec: + {{- if or .Values.proxyInit.closeWaitTimeoutSecs .Values.proxyInit.runAsRoot }} + allowPrivilegeEscalation: true + {{- else }} + allowPrivilegeEscalation: false + {{- end }} + readOnlyRootFilesystem: true + {{- if empty .Values.cniEnabled }} + allowedCapabilities: + - NET_ADMIN + - NET_RAW + {{- end}} + requiredDropCapabilities: + - ALL + hostNetwork: false + hostIPC: false + hostPID: false + seLinux: + rule: RunAsAny + runAsUser: + {{- if .Values.cniEnabled }} + rule: MustRunAsNonRoot + {{- else }} + rule: RunAsAny + {{- end }} + supplementalGroups: + rule: MustRunAs + ranges: + {{- if .Values.cniEnabled }} + - min: 10001 + max: 65535 + {{- else }} + - min: 1 + max: 65535 + {{- end }} + fsGroup: + rule: MustRunAs + ranges: + {{- if .Values.cniEnabled }} + - min: 10001 + max: 65535 + {{- else }} + - min: 1 + max: 65535 + {{- end }} + volumes: + - configMap + - emptyDir + - secret + - projected + - downwardAPI + - persistentVolumeClaim +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: linkerd-psp + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +rules: +- apiGroups: ['policy', 'extensions'] + resources: ['podsecuritypolicies'] + verbs: ['use'] + resourceNames: + - linkerd-{{.Release.Namespace}}-control-plane +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: linkerd-psp + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/control-plane-ns: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +roleRef: + kind: Role + name: linkerd-psp + apiGroup: rbac.authorization.k8s.io +subjects: +- kind: ServiceAccount + name: linkerd-destination + namespace: {{.Release.Namespace}} +{{ if not .Values.disableHeartBeat -}} +- kind: ServiceAccount + name: linkerd-heartbeat + namespace: {{.Release.Namespace}} +{{ end -}} +- kind: ServiceAccount + name: linkerd-identity + namespace: {{.Release.Namespace}} +- kind: ServiceAccount + name: linkerd-proxy-injector + namespace: {{.Release.Namespace}} +{{ end -}} diff --git a/linkerd/charts/linkerd-control-plane/values-ha.yaml b/linkerd/charts/linkerd-control-plane/values-ha.yaml new file mode 100644 index 0000000..3c0ce10 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/values-ha.yaml @@ -0,0 +1,60 @@ +# This values.yaml file contains the values needed to enable HA mode. +# Usage: +# helm install -f values-ha.yaml + +# -- Create PodDisruptionBudget resources for each control plane workload +enablePodDisruptionBudget: true + +# -- Specify a deployment strategy for each control plane workload +deploymentStrategy: + rollingUpdate: + maxUnavailable: 1 + maxSurge: 25% + +# -- add PodAntiAffinity to each control plane workload +enablePodAntiAffinity: true + +# nodeAffinity: + +# proxy configuration +proxy: + resources: + cpu: + request: 100m + memory: + limit: 250Mi + request: 20Mi + +# controller configuration +controllerReplicas: 3 +controllerResources: &controller_resources + cpu: &controller_resources_cpu + limit: "" + request: 100m + memory: + limit: 250Mi + request: 50Mi +destinationResources: *controller_resources + +# identity configuration +identityResources: + cpu: *controller_resources_cpu + memory: + limit: 250Mi + request: 10Mi + +# heartbeat configuration +heartbeatResources: *controller_resources + +# proxy injector configuration +proxyInjectorResources: *controller_resources +webhookFailurePolicy: Fail + +# service profile validator configuration +spValidatorResources: *controller_resources + +# policy controller configuration +policyControllerResources: *controller_resources + +# flag for linkerd check +highAvailability: true diff --git a/linkerd/charts/linkerd-control-plane/values.yaml b/linkerd/charts/linkerd-control-plane/values.yaml new file mode 100644 index 0000000..af85527 --- /dev/null +++ b/linkerd/charts/linkerd-control-plane/values.yaml @@ -0,0 +1,574 @@ +# Default values for linkerd. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# -- Kubernetes DNS Domain name to use +clusterDomain: cluster.local + +# -- The cluster networks for which service discovery is performed. This should +# include the pod and service networks, but need not include the node network. +# +# By default, all private networks are specified so that resolution works in +# typical Kubernetes environments. +clusterNetworks: "10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16" +# -- Docker image pull policy +imagePullPolicy: IfNotPresent +# -- Log level for the control plane components +controllerLogLevel: info +# -- Log format for the control plane components +controllerLogFormat: plain +# -- enables control plane tracing +controlPlaneTracing: false +# -- namespace to send control plane traces to +controlPlaneTracingNamespace: linkerd-jaeger +# -- control plane version. See Proxy section for proxy version +linkerdVersion: stable-2.14.10 +# -- default kubernetes deployment strategy +deploymentStrategy: + rollingUpdate: + maxUnavailable: 25% + maxSurge: 25% +# -- enables the use of EndpointSlice informers for the destination service; +# enableEndpointSlices should be set to true only if EndpointSlice K8s feature +# gate is on +enableEndpointSlices: true +# -- enables pod anti affinity creation on deployments for high availability +enablePodAntiAffinity: false +# -- enables the use of pprof endpoints on control plane component's admin +# servers +enablePprof: false +# -- enables the creation of pod disruption budgets for control plane components +enablePodDisruptionBudget: false +# -- enabling this omits the NET_ADMIN capability in the PSP +# and the proxy-init container when injecting the proxy; +# requires the linkerd-cni plugin to already be installed +cniEnabled: false +# -- Trust root certificate (ECDSA). It must be provided during install. +identityTrustAnchorsPEM: | +# -- Trust domain used for identity +# @default -- clusterDomain +identityTrustDomain: "" +kubeAPI: &kubeapi + # -- Maximum QPS sent to the kube-apiserver before throttling. + # See [token bucket rate limiter + # implementation](https://github.com/kubernetes/client-go/blob/v12.0.0/util/flowcontrol/throttle.go) + clientQPS: 100 + # -- Burst value over clientQPS + clientBurst: 200 +# -- Additional annotations to add to all pods +podAnnotations: {} +# -- Additional labels to add to all pods +podLabels: {} +# -- Labels to apply to all resources +commonLabels: {} +# -- Kubernetes priorityClassName for the Linkerd Pods +priorityClassName: "" +# -- Runtime Class Name for all the pods +runtimeClassName: "" + +# policy controller configuration +policyController: + image: + # -- Docker image for the policy controller + name: cr.l5d.io/linkerd/policy-controller + # -- Pull policy for the proxy container Docker image + # @default -- imagePullPolicy + pullPolicy: "" + # -- Tag for the proxy container Docker image + # @default -- linkerdVersion + version: "" + + # -- Log level for the policy controller + logLevel: info + + # -- The networks from which probes are performed. + # + # By default, all networks are allowed so that all probes are authorized. + probeNetworks: + - 0.0.0.0/0 + + # -- policy controller resource requests & limits + # @default -- destinationResources + resources: + cpu: + # -- Maximum amount of CPU units that the policy controller can use + limit: "" + # -- Amount of CPU units that the policy controller requests + request: "" + memory: + # -- Maximum amount of memory that the policy controller can use + limit: "" + # -- Maximum amount of memory that the policy controller requests + request: "" + ephemeral-storage: + # -- Maximum amount of ephemeral storage that the policy controller can use + limit: "" + # -- Amount of ephemeral storage that the policy controller requests + request: "" + +# proxy configuration +proxy: + # -- Enable service profiles for non-Kubernetes services + enableExternalProfiles: false + # -- Maximum time allowed for the proxy to establish an outbound TCP + # connection + outboundConnectTimeout: 1000ms + # -- Maximum time allowed for the proxy to establish an inbound TCP + # connection + inboundConnectTimeout: 100ms + # -- Maximum time allowed before an unused outbound discovery result + # is evicted from the cache + outboundDiscoveryCacheUnusedTimeout: "5s" + # -- Maximum time allowed before an unused inbound discovery result + # is evicted from the cache + inboundDiscoveryCacheUnusedTimeout: "90s" + # -- When set to true, disables the protocol detection timeout on the + # outbound side of the proxy by setting it to a very high value + disableOutboundProtocolDetectTimeout: false + # -- When set to true, disables the protocol detection timeout on the inbound + # side of the proxy by setting it to a very high value + disableInboundProtocolDetectTimeout: false + image: + # -- Docker image for the proxy + name: cr.l5d.io/linkerd/proxy + # -- Pull policy for the proxy container Docker image + # @default -- imagePullPolicy + pullPolicy: "" + # -- Tag for the proxy container Docker image + # @default -- linkerdVersion + version: "" + # -- Log level for the proxy + logLevel: warn,linkerd=info,trust_dns=error + # -- Log format (`plain` or `json`) for the proxy + logFormat: plain + ports: + # -- Admin port for the proxy container + admin: 4191 + # -- Control port for the proxy container + control: 4190 + # -- Inbound port for the proxy container + inbound: 4143 + # -- Outbound port for the proxy container + outbound: 4140 + # -- The `cpu.limit` and `cores` should be kept in sync. The value of `cores` + # must be an integer and should typically be set by rounding up from the + # limit. E.g. if cpu.limit is '1500m', cores should be 2. + cores: 0 + resources: + cpu: + # -- Maximum amount of CPU units that the proxy can use + limit: "" + # -- Amount of CPU units that the proxy requests + request: "" + memory: + # -- Maximum amount of memory that the proxy can use + limit: "" + # -- Maximum amount of memory that the proxy requests + request: "" + ephemeral-storage: + # -- Maximum amount of ephemeral storage that the proxy can use + limit: "" + # -- Amount of ephemeral storage that the proxy requests + request: "" + # -- User id under which the proxy runs + uid: 2102 + # -- If set the injected proxy sidecars in the data plane will stay alive for + # at least the given period before receiving the SIGTERM signal from + # Kubernetes but no longer than the pod's `terminationGracePeriodSeconds`. + # See [Lifecycle + # hooks](https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks) + # for more info on container lifecycle hooks. + waitBeforeExitSeconds: 0 + # -- If set, the application container will not start until the proxy is + # ready + await: true + requireIdentityOnInboundPorts: "" + # -- Default set of opaque ports + # - SMTP (25,587) server-first + # - MYSQL (3306) server-first + # - Galera (4444) server-first + # - PostgreSQL (5432) server-first + # - Redis (6379) server-first + # - ElasticSearch (9300) server-first + # - Memcached (11211) clients do not issue any preamble, which breaks detection + opaquePorts: "25,587,3306,4444,5432,6379,9300,11211" + # -- Grace period for graceful proxy shutdowns. If this timeout elapses before all open connections have completed, the proxy will terminate forcefully, closing any remaining connections. + shutdownGracePeriod: "" + # -- The default allow policy to use when no `Server` selects a pod. One of: "all-authenticated", + # "all-unauthenticated", "cluster-authenticated", "cluster-unauthenticated", "deny" + # @default -- "all-unauthenticated" + defaultInboundPolicy: "all-unauthenticated" + +# proxy-init configuration +proxyInit: + # -- Variant of iptables that will be used to configure routing. Currently, + # proxy-init can be run either in 'nft' or in 'legacy' mode. The mode will + # control which utility binary will be called. The host must support + # whichever mode will be used + iptablesMode: "legacy" + # -- Default set of inbound ports to skip via iptables + # - Galera (4567,4568) + ignoreInboundPorts: "4567,4568" + # -- Default set of outbound ports to skip via iptables + # - Galera (4567,4568) + ignoreOutboundPorts: "4567,4568" + # -- Default set of ports to skip via iptables for control plane + # components so they can communicate with the Kubernetes API Server + kubeAPIServerPorts: "443,6443" + # -- Comma-separated list of subnets in valid CIDR format that should be skipped by the proxy + skipSubnets: "" + # -- Log level for the proxy-init + # @default -- info + logLevel: "" + # -- Log format (`plain` or `json`) for the proxy-init + # @default -- plain + logFormat: "" + image: + # -- Docker image for the proxy-init container + name: cr.l5d.io/linkerd/proxy-init + # -- Pull policy for the proxy-init container Docker image + # @default -- imagePullPolicy + pullPolicy: "" + # -- Tag for the proxy-init container Docker image + version: v2.2.3 + resources: + cpu: + # -- Maximum amount of CPU units that the proxy-init container can use + limit: 100m + # -- Amount of CPU units that the proxy-init container requests + request: 100m + memory: + # -- Maximum amount of memory that the proxy-init container can use + limit: 20Mi + # -- Amount of memory that the proxy-init container requests + request: 20Mi + ephemeral-storage: + # -- Maximum amount of ephemeral storage that the proxy-init container can use + limit: "" + # -- Amount of ephemeral storage that the proxy-init container requests + request: "" + closeWaitTimeoutSecs: 0 + # -- Privileged mode allows the container processes to inherit all security + # capabilities and bypass any security limitations enforced by the kubelet. + # When used with 'runAsRoot: true', the container will behave exactly as if + # it was running as root on the host. May escape cgroup limits and see other + # processes and devices on the host. + # @default -- false + privileged: false + # -- Allow overriding the runAsNonRoot behaviour () + runAsRoot: false + # -- This value is used only if runAsRoot is false; otherwise runAsUser will be 0 + runAsUser: 65534 + xtMountPath: + mountPath: /run + name: linkerd-proxy-init-xtables-lock + +# network validator configuration +# This runs on a host that uses iptables to reroute network traffic. The validator +# ensures that iptables is correctly routing requests before we start linkerd. +networkValidator: + # -- Log level for the network-validator + # @default -- debug + logLevel: debug + # -- Log format (`plain` or `json`) for network-validator + # @default -- plain + logFormat: plain + # -- Address to which the network-validator will attempt to connect. we expect this to be rewritten + connectAddr: "1.1.1.1:20001" + # -- Address to which network-validator listens to requests from itself + listenAddr: "0.0.0.0:4140" + # -- Timeout before network-validator fails to validate the pod's network connectivity + timeout: "10s" + # -- Include a securityContext in the network-validator pod spec + enableSecurityContext: true + +# -- For Private docker registries, authentication is needed. +# Registry secrets are applied to the respective service accounts +imagePullSecrets: [] +# - name: my-private-docker-registry-login-secret + +# -- Allow proxies to perform transparent HTTP/2 upgrading +enableH2Upgrade: true + +# -- Add a PSP resource and bind it to the control plane ServiceAccounts. Note +# PSP has been deprecated since k8s v1.21 +enablePSP: false + +# -- Failure policy for the proxy injector +webhookFailurePolicy: Ignore + +# controllerImage -- Docker image for the destination and identity components +controllerImage: cr.l5d.io/linkerd/controller +# -- Optionally allow a specific container image Tag (or SHA) to be specified for the controllerImage. +controllerImageVersion: "" + +# -- Number of replicas for each control plane pod +controllerReplicas: 1 +# -- User ID for the control plane components +controllerUID: 2103 + +# destination configuration +# set resources for the sp-validator and its linkerd proxy respectively +# see proxy.resources for details. +# destinationResources -- CPU, Memory and Ephemeral Storage resources required by destination (see `proxy.resources` for sub-fields) +#destinationResources: +#destinationProxyResources -- CPU, Memory and Ephemeral Storage resources required by proxy +# injected into destination pod (see `proxy.resources` for sub-fields) +#destinationProxyResources: + +# debug configuration +debugContainer: + image: + # -- Docker image for the debug container + name: cr.l5d.io/linkerd/debug + # -- Pull policy for the debug container Docker image + # @default -- imagePullPolicy + pullPolicy: "" + # -- Tag for the debug container Docker image + # @default -- linkerdVersion + version: "" + +identity: + # -- If the linkerd-identity-trust-roots ConfigMap has already been created + externalCA: false + + # -- Use [Service Account token Volume projection](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#service-account-token-volume-projection) for pod validation instead of the default token + serviceAccountTokenProjection: true + + issuer: + scheme: linkerd.io/tls + + # -- Amount of time to allow for clock skew within a Linkerd cluster + clockSkewAllowance: 20s + + # -- Amount of time for which the Identity issuer should certify identity + issuanceLifetime: 24h0m0s + + # -- Which scheme is used for the identity issuer secret format + tls: + # -- Issuer certificate (ECDSA). It must be provided during install. + crtPEM: | + + # -- Key for the issuer certificate (ECDSA). It must be provided during + # install + keyPEM: | + + kubeAPI: *kubeapi + +# -|- CPU, Memory and Ephemeral Storage resources required by the identity controller (see `proxy.resources` for sub-fields) +#identityResources: +# -|- CPU, Memory and Ephemeral Storage resources required by proxy injected into identity pod (see `proxy.resources` for sub-fields) +#identityProxyResources: + +# heartbeat configuration +# disableHeartBeat -- Set to true to not start the heartbeat cronjob +disableHeartBeat: false +# -- Config for the heartbeat cronjob +# heartbeatSchedule: "0 0 * * *" + +# proxy injector configuration +proxyInjector: + # -- Do not create a secret resource for the proxyInjector webhook. + # If this is set to `true`, the value `proxyInjector.caBundle` must be set + # or the ca bundle must injected with cert-manager ca injector using + # `proxyInjector.injectCaFrom` or `proxyInjector.injectCaFromSecret` (see below). + externalSecret: false + + # -- Namespace selector used by admission webhook. + namespaceSelector: + matchExpressions: + - key: config.linkerd.io/admission-webhooks + operator: NotIn + values: + - disabled + - key: kubernetes.io/metadata.name + operator: NotIn + values: + - kube-system + - cert-manager + + # -- Object selector used by admission webhook. + objectSelector: + matchExpressions: + - key: linkerd.io/control-plane-component + operator: DoesNotExist + - key: linkerd.io/cni-resource + operator: DoesNotExist + + # -- Certificate for the proxy injector. If not provided and not using an external secret + # then Helm will generate one. + crtPEM: | + + # -- Certificate key for the proxy injector. If not provided and not using an external secret + # then Helm will generate one. + keyPEM: | + + # -- Bundle of CA certificates for proxy injector. + # If not provided nor injected with cert-manager, + # then Helm will use the certificate generated for `proxyInjector.crtPEM`. + # If `proxyInjector.externalSecret` is set to true, this value, injectCaFrom, or + # injectCaFromSecret must be set, as no certificate will be generated. + # See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector) for more information. + caBundle: | + + # -- Inject the CA bundle from a cert-manager Certificate. + # See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-certificate-resource) + # for more information. + injectCaFrom: "" + + # -- Inject the CA bundle from a Secret. + # If set, the `cert-manager.io/inject-ca-from-secret` annotation will be added to the webhook. + # The Secret must have the CA Bundle stored in the `ca.crt` key and have + # the `cert-manager.io/allow-direct-injection` annotation set to `true`. + # See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-secret-resource) + # for more information. + injectCaFromSecret: "" + +# -|- CPU, Memory and Ephemeral Storage resources required by the proxy injector (see +#`proxy.resources` for sub-fields) +#proxyInjectorResources: +#-|- CPU, Memory and Ephemeral Storage resources required by proxy injected into the proxy injector +#pod (see `proxy.resources` for sub-fields) +#proxyInjectorProxyResources: + +# service profile validator configuration +profileValidator: + # -- Do not create a secret resource for the profileValidator webhook. + # If this is set to `true`, the value `proxyInjector.caBundle` must be set + # or the ca bundle must injected with cert-manager ca injector using + # `proxyInjector.injectCaFrom` or `proxyInjector.injectCaFromSecret` (see below). + externalSecret: false + + # -- Namespace selector used by admission webhook + namespaceSelector: + matchExpressions: + - key: config.linkerd.io/admission-webhooks + operator: NotIn + values: + - disabled + + # -- Certificate for the service profile validator. If not provided and not using an external secret + # then Helm will generate one. + crtPEM: | + + # -- Certificate key for the service profile validator. If not provided and not using an external secret + # then Helm will generate one. + keyPEM: | + + # -- Bundle of CA certificates for proxy injector. + # If not provided nor injected with cert-manager, + # then Helm will use the certificate generated for `profileValidator.crtPEM`. + # If `profileValidator.externalSecret` is set to true, this value, injectCaFrom, or + # injectCaFromSecret must be set, as no certificate will be generated. + # See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector) for more information. + caBundle: | + + # -- Inject the CA bundle from a cert-manager Certificate. + # See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-certificate-resource) + # for more information. + injectCaFrom: "" + + # -- Inject the CA bundle from a Secret. + # If set, the `cert-manager.io/inject-ca-from-secret` annotation will be added to the webhook. + # The Secret must have the CA Bundle stored in the `ca.crt` key and have + # the `cert-manager.io/allow-direct-injection` annotation set to `true`. + # See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-secret-resource) + # for more information. + injectCaFromSecret: "" + +# policy validator configuration +policyValidator: + # -- Do not create a secret resource for the policyValidator webhook. + # If this is set to `true`, the value `policyValidator.caBundle` must be set + # or the ca bundle must injected with cert-manager ca injector using + # `policyValidator.injectCaFrom` or `policyValidator.injectCaFromSecret` (see below). + externalSecret: false + + # -- Namespace selector used by admission webhook + namespaceSelector: + matchExpressions: + - key: config.linkerd.io/admission-webhooks + operator: NotIn + values: + - disabled + + # -- Certificate for the policy validator. If not provided and not using an external secret + # then Helm will generate one. + crtPEM: | + + # -- Certificate key for the policy validator. If not provided and not using an external secret + # then Helm will generate one. + keyPEM: | + + # -- Bundle of CA certificates for proxy injector. + # If not provided nor injected with cert-manager, + # then Helm will use the certificate generated for `policyValidator.crtPEM`. + # If `policyValidator.externalSecret` is set to true, this value, injectCaFrom, or + # injectCaFromSecret must be set, as no certificate will be generated. + # See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector) for more information. + caBundle: | + + # -- Inject the CA bundle from a cert-manager Certificate. + # See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-certificate-resource) + # for more information. + injectCaFrom: "" + + # -- Inject the CA bundle from a Secret. + # If set, the `cert-manager.io/inject-ca-from-secret` annotation will be added to the webhook. + # The Secret must have the CA Bundle stored in the `ca.crt` key and have + # the `cert-manager.io/allow-direct-injection` annotation set to `true`. + # See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-secret-resource) + # for more information. + injectCaFromSecret: "" + +# -|- CPU, Memory and Ephemeral Storage resources required by the policy controller +#policyControllerResources: + +# -- NodeSelector section, See the [K8S +# documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) +# for more information +nodeSelector: + kubernetes.io/os: linux + +# -|- CPU, Memory and Ephemeral Storage resources required by the SP validator (see +#`proxy.resources` for sub-fields) +#spValidatorResources: + +# -|- Tolerations section, See the +# [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) +# for more information +#tolerations: + +# -|- NodeAffinity section, See the +# [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity) +# for more information +#nodeAffinity: + +# -- url of external prometheus instance (used for the heartbeat) +prometheusUrl: "" + +# Prometheus Operator PodMonitor configuration +podMonitor: + # -- Enables the creation of Prometheus Operator [PodMonitor](https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1.PodMonitor) + enabled: false + # -- Interval at which metrics should be scraped + scrapeInterval: 10s + # -- Iimeout after which the scrape is ended + scrapeTimeout: 10s + # -- Labels to apply to all pod Monitors + labels: {} + controller: + # -- Enables the creation of PodMonitor for the control-plane + enabled: true + # -- Selector to select which namespaces the Endpoints objects are discovered from + namespaceSelector: | + matchNames: + - {{ .Release.Namespace }} + - linkerd-viz + - linkerd-jaeger + serviceMirror: + # -- Enables the creation of PodMonitor for the Service Mirror component + enabled: true + proxy: + # -- Enables the creation of PodMonitor for the data-plane + enabled: true diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/.helmignore b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/.helmignore new file mode 100644 index 0000000..79c90a8 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +OWNERS +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/Chart.lock b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/Chart.lock new file mode 100644 index 0000000..a62a030 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: partials + repository: file://../partials + version: 0.1.0 +digest: sha256:8e42f9c9d4a2dc883f17f94d6044c97518ced19ad0922f47b8760e47135369ba +generated: "2021-08-17T10:42:52.610449255-05:00" diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/Chart.yaml b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/Chart.yaml new file mode 100644 index 0000000..e368bc6 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +dependencies: +- name: partials + repository: file://../partials + version: 0.1.0 +description: 'Linkerd gives you observability, reliability, and security for your + microservices — with no code change required. ' +home: https://linkerd.io +icon: https://linkerd.io/images/logo-only-200h.png +keywords: +- service-mesh +kubeVersion: '>=1.21.0-0' +maintainers: +- email: cncf-linkerd-dev@lists.cncf.io + name: Linkerd authors + url: https://linkerd.io/ +name: linkerd-crds +sources: +- https://github.com/linkerd/linkerd2/ +type: application +version: 1.8.0 diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/README.md b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/README.md new file mode 100644 index 0000000..28cf91e --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/README.md @@ -0,0 +1,76 @@ +# linkerd-crds + +Linkerd gives you observability, reliability, and security +for your microservices — with no code change required. + +![Version: 1.8.0](https://img.shields.io/badge/Version-1.8.0-informational?style=flat-square) +![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) + +**Homepage:** + +## Quickstart and documentation + +You can run Linkerd on any Kubernetes 1.21+ cluster in a matter of seconds. See +the [Linkerd Getting Started Guide][getting-started] for how. + +For more comprehensive documentation, start with the [Linkerd +docs][linkerd-docs]. + +## Adding Linkerd's Helm repository + +```bash +# To add the repo for Linkerd stable releases: +helm repo add linkerd https://helm.linkerd.io/stable +# To add the repo for Linkerd edge releases: +helm repo add linkerd-edge https://helm.linkerd.io/edge +``` + +The following instructions use the `linkerd` repo. For installing an edge +release, just replace with `linkerd-edge`. + +## Installing the linkerd-crds chart + +This installs the `linkerd-crds` chart, which only persists the CRDs that +Linkerd requires. + +After installing this chart, you need then to install the +`linkerd-control-plane` chart in the same namespace, which provides all the +linkerd core control components. + +```bash +helm install linkerd-crds -n linkerd --create-namespace linkerd/linkerd-crds +``` + +## Get involved + +* Check out Linkerd's source code at [GitHub][linkerd2]. +* Join Linkerd's [user mailing list][linkerd-users], [developer mailing + list][linkerd-dev], and [announcements mailing list][linkerd-announce]. +* Follow [@linkerd][twitter] on Twitter. +* Join the [Linkerd Slack][slack]. + +[getting-started]: https://linkerd.io/2/getting-started/ +[linkerd2]: https://github.com/linkerd/linkerd2 +[linkerd-announce]: https://lists.cncf.io/g/cncf-linkerd-announce +[linkerd-dev]: https://lists.cncf.io/g/cncf-linkerd-dev +[linkerd-docs]: https://linkerd.io/2/overview/ +[linkerd-users]: https://lists.cncf.io/g/cncf-linkerd-users +[slack]: http://slack.linkerd.io +[twitter]: https://twitter.com/linkerd + +## Requirements + +Kubernetes: `>=1.21.0-0` + +| Repository | Name | Version | +|------------|------|---------| +| file://../partials | partials | 0.1.0 | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| enableHttpRoutes | bool | `true` | | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/README.md.gotmpl b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/README.md.gotmpl new file mode 100644 index 0000000..b396a6f --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/README.md.gotmpl @@ -0,0 +1,64 @@ +{{ template "chart.header" . }} +{{ template "chart.description" . }} + +{{ template "chart.versionBadge" . }} +{{ template "chart.typeBadge" . }} +{{ template "chart.appVersionBadge" . }} + +{{ template "chart.homepageLine" . }} + +## Quickstart and documentation + +You can run Linkerd on any Kubernetes 1.21+ cluster in a matter of seconds. See +the [Linkerd Getting Started Guide][getting-started] for how. + +For more comprehensive documentation, start with the [Linkerd +docs][linkerd-docs]. + +## Adding Linkerd's Helm repository + +```bash +# To add the repo for Linkerd stable releases: +helm repo add linkerd https://helm.linkerd.io/stable +# To add the repo for Linkerd edge releases: +helm repo add linkerd-edge https://helm.linkerd.io/edge +``` + +The following instructions use the `linkerd` repo. For installing an edge +release, just replace with `linkerd-edge`. + +## Installing the linkerd-crds chart + +This installs the `linkerd-crds` chart, which only persists the CRDs that +Linkerd requires. + +After installing this chart, you need then to install the +`linkerd-control-plane` chart in the same namespace, which provides all the +linkerd core control components. + +```bash +helm install linkerd-crds -n linkerd --create-namespace linkerd/linkerd-crds +``` + +## Get involved + +* Check out Linkerd's source code at [GitHub][linkerd2]. +* Join Linkerd's [user mailing list][linkerd-users], [developer mailing + list][linkerd-dev], and [announcements mailing list][linkerd-announce]. +* Follow [@linkerd][twitter] on Twitter. +* Join the [Linkerd Slack][slack]. + +[getting-started]: https://linkerd.io/2/getting-started/ +[linkerd2]: https://github.com/linkerd/linkerd2 +[linkerd-announce]: https://lists.cncf.io/g/cncf-linkerd-announce +[linkerd-dev]: https://lists.cncf.io/g/cncf-linkerd-dev +[linkerd-docs]: https://linkerd.io/2/overview/ +[linkerd-users]: https://lists.cncf.io/g/cncf-linkerd-users +[slack]: http://slack.linkerd.io +[twitter]: https://twitter.com/linkerd + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/.helmignore b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/.helmignore new file mode 100644 index 0000000..f0c1319 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/Chart.yaml b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/Chart.yaml new file mode 100644 index 0000000..23cfc16 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +description: 'A Helm chart containing Linkerd partial templates, depended by the ''linkerd'' + and ''patch'' charts. ' +name: partials +version: 0.1.0 diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/README.md b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/README.md new file mode 100644 index 0000000..e5fbd39 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/README.md @@ -0,0 +1,9 @@ +# partials + +A Helm chart containing Linkerd partial templates, +depended by the 'linkerd' and 'patch' charts. + +![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/README.md.gotmpl b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/README.md.gotmpl new file mode 100644 index 0000000..37f5101 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/README.md.gotmpl @@ -0,0 +1,14 @@ +{{ template "chart.header" . }} +{{ template "chart.description" . }} + +{{ template "chart.versionBadge" . }} +{{ template "chart.typeBadge" . }} +{{ template "chart.appVersionBadge" . }} + +{{ template "chart.homepageLine" . }} + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/NOTES.txt b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/NOTES.txt new file mode 100644 index 0000000..e69de29 diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_affinity.tpl b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_affinity.tpl new file mode 100644 index 0000000..5dde1da --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_affinity.tpl @@ -0,0 +1,38 @@ +{{ define "linkerd.pod-affinity" -}} +podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: {{ default "linkerd.io/control-plane-component" .label }} + operator: In + values: + - {{ .component }} + topologyKey: topology.kubernetes.io/zone + weight: 100 + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: {{ default "linkerd.io/control-plane-component" .label }} + operator: In + values: + - {{ .component }} + topologyKey: kubernetes.io/hostname +{{- end }} + +{{ define "linkerd.node-affinity" -}} +nodeAffinity: +{{- toYaml .Values.nodeAffinity | trim | nindent 2 }} +{{- end }} + +{{ define "linkerd.affinity" -}} +{{- if or .Values.enablePodAntiAffinity .Values.nodeAffinity -}} +affinity: +{{- end }} +{{- if .Values.enablePodAntiAffinity -}} +{{- include "linkerd.pod-affinity" . | nindent 2 }} +{{- end }} +{{- if .Values.nodeAffinity -}} +{{- include "linkerd.node-affinity" . | nindent 2 }} +{{- end }} +{{- end }} diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_capabilities.tpl b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_capabilities.tpl new file mode 100644 index 0000000..a595d74 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_capabilities.tpl @@ -0,0 +1,16 @@ +{{- define "partials.proxy.capabilities" -}} +capabilities: + {{- if .Values.proxy.capabilities.add }} + add: + {{- toYaml .Values.proxy.capabilities.add | trim | nindent 4 }} + {{- end }} + {{- if .Values.proxy.capabilities.drop }} + drop: + {{- toYaml .Values.proxy.capabilities.drop | trim | nindent 4 }} + {{- end }} +{{- end -}} + +{{- define "partials.proxy-init.capabilities.drop" -}} +drop: +{{ toYaml .Values.proxyInit.capabilities.drop | trim }} +{{- end -}} diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_debug.tpl b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_debug.tpl new file mode 100644 index 0000000..cf0eb14 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_debug.tpl @@ -0,0 +1,6 @@ +{{- define "partials.debug" -}} +image: {{.Values.debugContainer.image.name}}:{{.Values.debugContainer.image.version | default .Values.linkerdVersion}} +imagePullPolicy: {{.Values.debugContainer.image.pullPolicy | default .Values.imagePullPolicy}} +name: linkerd-debug +terminationMessagePolicy: FallbackToLogsOnError +{{- end -}} diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_helpers.tpl b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_helpers.tpl new file mode 100644 index 0000000..b6cdc34 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_helpers.tpl @@ -0,0 +1,14 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Splits a coma separated list into a list of string values. +For example "11,22,55,44" will become "11","22","55","44" +*/}} +{{- define "partials.splitStringList" -}} +{{- if gt (len (toString .)) 0 -}} +{{- $ports := toString . | splitList "," -}} +{{- $last := sub (len $ports) 1 -}} +{{- range $i,$port := $ports -}} +"{{$port}}"{{ternary "," "" (ne $i $last)}} +{{- end -}} +{{- end -}} +{{- end -}} diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_metadata.tpl b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_metadata.tpl new file mode 100644 index 0000000..04d2f1b --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_metadata.tpl @@ -0,0 +1,17 @@ +{{- define "partials.annotations.created-by" -}} +linkerd.io/created-by: {{ .Values.cliVersion | default (printf "linkerd/helm %s" ( (.Values.image).version | default .Values.linkerdVersion)) }} +{{- end -}} + +{{- define "partials.proxy.annotations" -}} +linkerd.io/proxy-version: {{.Values.proxy.image.version | default .Values.linkerdVersion}} +cluster-autoscaler.kubernetes.io/safe-to-evict: "true" +linkerd.io/trust-root-sha256: {{ .Values.identityTrustAnchorsPEM | sha256sum }} +{{- end -}} + +{{/* +To add labels to the control-plane components, instead update at individual component manifests as +adding here would also update `spec.selector.matchLabels` which are immutable and would fail upgrades. +*/}} +{{- define "partials.proxy.labels" -}} +linkerd.io/proxy-{{.workloadKind}}: {{.component}} +{{- end -}} diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_network-validator.tpl b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_network-validator.tpl new file mode 100644 index 0000000..58f36e6 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_network-validator.tpl @@ -0,0 +1,32 @@ +{{- define "partials.network-validator" -}} +name: linkerd-network-validator +image: {{.Values.proxy.image.name}}:{{.Values.proxy.image.version | default .Values.linkerdVersion }} +imagePullPolicy: {{.Values.proxy.image.pullPolicy | default .Values.imagePullPolicy}} +{{ include "partials.resources" .Values.proxyInit.resources }} +{{- if or .Values.networkValidator.enableSecurityContext }} +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 65534 + seccompProfile: + type: RuntimeDefault +{{- end }} +command: + - /usr/lib/linkerd/linkerd2-network-validator +args: + - --log-format + - {{ .Values.networkValidator.logFormat }} + - --log-level + - {{ .Values.networkValidator.logLevel }} + - --connect-addr + - {{ .Values.networkValidator.connectAddr }} + - --listen-addr + - {{ .Values.networkValidator.listenAddr }} + - --timeout + - {{ .Values.networkValidator.timeout }} + +{{- end -}} diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_nodeselector.tpl b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_nodeselector.tpl new file mode 100644 index 0000000..4cde0ab --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_nodeselector.tpl @@ -0,0 +1,4 @@ +{{- define "linkerd.node-selector" -}} +nodeSelector: +{{- toYaml .Values.nodeSelector | trim | nindent 2 }} +{{- end -}} diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_proxy-config-ann.tpl b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_proxy-config-ann.tpl new file mode 100644 index 0000000..9651b3b --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_proxy-config-ann.tpl @@ -0,0 +1,18 @@ +{{- define "partials.proxy.config.annotations" -}} +{{- with .cpu }} +{{- with .request -}} +config.linkerd.io/proxy-cpu-request: {{. | quote}} +{{end}} +{{- with .limit -}} +config.linkerd.io/proxy-cpu-limit: {{. | quote}} +{{- end}} +{{- end}} +{{- with .memory }} +{{- with .request }} +config.linkerd.io/proxy-memory-request: {{. | quote}} +{{end}} +{{- with .limit -}} +config.linkerd.io/proxy-memory-limit: {{. | quote}} +{{- end}} +{{- end }} +{{- end }} diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_proxy-init.tpl b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_proxy-init.tpl new file mode 100644 index 0000000..91cc96e --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_proxy-init.tpl @@ -0,0 +1,89 @@ +{{- define "partials.proxy-init" -}} +args: +{{- if (.Values.proxyInit.iptablesMode | default "legacy" | eq "nft") }} +- --firewall-bin-path +- "iptables-nft" +- --firewall-save-bin-path +- "iptables-nft-save" +{{- else if not (eq .Values.proxyInit.iptablesMode "legacy") }} +{{ fail (printf "Unsupported value \"%s\" for proxyInit.iptablesMode\nValid values: [\"nft\", \"legacy\"]" .Values.proxyInit.iptablesMode) }} +{{- end }} +- --incoming-proxy-port +- {{.Values.proxy.ports.inbound | quote}} +- --outgoing-proxy-port +- {{.Values.proxy.ports.outbound | quote}} +- --proxy-uid +- {{.Values.proxy.uid | quote}} +- --inbound-ports-to-ignore +- "{{.Values.proxy.ports.control}},{{.Values.proxy.ports.admin}}{{ternary (printf ",%s" (.Values.proxyInit.ignoreInboundPorts | toString)) "" (not (empty .Values.proxyInit.ignoreInboundPorts)) }}" +{{- if .Values.proxyInit.ignoreOutboundPorts }} +- --outbound-ports-to-ignore +- {{.Values.proxyInit.ignoreOutboundPorts | quote}} +{{- end }} +{{- if .Values.proxyInit.closeWaitTimeoutSecs }} +- --timeout-close-wait-secs +- {{ .Values.proxyInit.closeWaitTimeoutSecs | quote}} +{{- end }} +{{- if .Values.proxyInit.logFormat }} +- --log-format +- {{ .Values.proxyInit.logFormat }} +{{- end }} +{{- if .Values.proxyInit.logLevel }} +- --log-level +- {{ .Values.proxyInit.logLevel }} +{{- end }} +{{- if .Values.proxyInit.skipSubnets }} +- --subnets-to-ignore +- {{ .Values.proxyInit.skipSubnets | quote }} +{{- end }} +image: {{.Values.proxyInit.image.name}}:{{.Values.proxyInit.image.version}} +imagePullPolicy: {{.Values.proxyInit.image.pullPolicy | default .Values.imagePullPolicy}} +name: linkerd-init +{{ include "partials.resources" .Values.proxyInit.resources }} +securityContext: + {{- if or .Values.proxyInit.closeWaitTimeoutSecs .Values.proxyInit.privileged }} + allowPrivilegeEscalation: true + {{- else }} + allowPrivilegeEscalation: false + {{- end }} + capabilities: + add: + - NET_ADMIN + - NET_RAW + {{- if .Values.proxyInit.capabilities -}} + {{- if .Values.proxyInit.capabilities.add }} + {{- toYaml .Values.proxyInit.capabilities.add | trim | nindent 4 }} + {{- end }} + {{- if .Values.proxyInit.capabilities.drop -}} + {{- include "partials.proxy-init.capabilities.drop" . | nindent 4 -}} + {{- end }} + {{- end }} + {{- if or .Values.proxyInit.closeWaitTimeoutSecs .Values.proxyInit.privileged }} + privileged: true + {{- else }} + privileged: false + {{- end }} + {{- if .Values.proxyInit.runAsRoot }} + runAsNonRoot: false + runAsUser: 0 + {{- else }} + runAsNonRoot: true + runAsUser: {{ .Values.proxyInit.runAsUser | int | eq 0 | ternary 65534 .Values.proxyInit.runAsUser }} + {{- end }} + readOnlyRootFilesystem: true + seccompProfile: + type: RuntimeDefault +terminationMessagePolicy: FallbackToLogsOnError +{{- if or (not .Values.cniEnabled) .Values.proxyInit.saMountPath }} +volumeMounts: +{{- end -}} +{{- if not .Values.cniEnabled }} +- mountPath: {{.Values.proxyInit.xtMountPath.mountPath}} + name: {{.Values.proxyInit.xtMountPath.name}} +{{- end -}} +{{- if .Values.proxyInit.saMountPath }} +- mountPath: {{.Values.proxyInit.saMountPath.mountPath}} + name: {{.Values.proxyInit.saMountPath.name}} + readOnly: {{.Values.proxyInit.saMountPath.readOnly}} +{{- end -}} +{{- end -}} diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_proxy.tpl b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_proxy.tpl new file mode 100644 index 0000000..f5dd4c2 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_proxy.tpl @@ -0,0 +1,215 @@ +{{ define "partials.proxy" -}} +{{- $trustDomain := (.Values.identityTrustDomain | default .Values.clusterDomain) -}} +env: +- name: _pod_name + valueFrom: + fieldRef: + fieldPath: metadata.name +- name: _pod_ns + valueFrom: + fieldRef: + fieldPath: metadata.namespace +- name: _pod_nodeName + valueFrom: + fieldRef: + fieldPath: spec.nodeName +{{- if .Values.proxy.cores }} +- name: LINKERD2_PROXY_CORES + value: {{.Values.proxy.cores | quote}} +{{- end }} +{{ if .Values.proxy.requireIdentityOnInboundPorts -}} +- name: LINKERD2_PROXY_INBOUND_PORTS_REQUIRE_IDENTITY + value: {{.Values.proxy.requireIdentityOnInboundPorts | quote}} +{{ end -}} +{{ if .Values.proxy.requireTLSOnInboundPorts -}} +- name: LINKERD2_PROXY_INBOUND_PORTS_REQUIRE_TLS + value: {{.Values.proxy.requireTLSOnInboundPorts | quote}} +{{ end -}} +- name: LINKERD2_PROXY_LOG + value: {{.Values.proxy.logLevel | quote}} +- name: LINKERD2_PROXY_LOG_FORMAT + value: {{.Values.proxy.logFormat | quote}} +- name: LINKERD2_PROXY_DESTINATION_SVC_ADDR + value: {{ternary "localhost.:8086" (printf "linkerd-dst-headless.%s.svc.%s.:8086" .Release.Namespace .Values.clusterDomain) (eq (toString .Values.proxy.component) "linkerd-destination")}} +- name: LINKERD2_PROXY_DESTINATION_PROFILE_NETWORKS + value: {{.Values.clusterNetworks | quote}} +- name: LINKERD2_PROXY_POLICY_SVC_ADDR + value: {{ternary "localhost.:8090" (printf "linkerd-policy.%s.svc.%s.:8090" .Release.Namespace .Values.clusterDomain) (eq (toString .Values.proxy.component) "linkerd-destination")}} +- name: LINKERD2_PROXY_POLICY_WORKLOAD + value: "$(_pod_ns):$(_pod_name)" +- name: LINKERD2_PROXY_INBOUND_DEFAULT_POLICY + value: {{.Values.proxy.defaultInboundPolicy}} +- name: LINKERD2_PROXY_POLICY_CLUSTER_NETWORKS + value: {{.Values.clusterNetworks | quote}} +{{ if .Values.proxy.inboundConnectTimeout -}} +- name: LINKERD2_PROXY_INBOUND_CONNECT_TIMEOUT + value: {{.Values.proxy.inboundConnectTimeout | quote}} +{{ end -}} +{{ if .Values.proxy.outboundConnectTimeout -}} +- name: LINKERD2_PROXY_OUTBOUND_CONNECT_TIMEOUT + value: {{.Values.proxy.outboundConnectTimeout | quote}} +{{ end -}} +{{ if .Values.proxy.outboundDiscoveryCacheUnusedTimeout -}} +- name: LINKERD2_PROXY_OUTBOUND_DISCOVERY_IDLE_TIMEOUT + value: {{.Values.proxy.outboundDiscoveryCacheUnusedTimeout | quote}} +{{ end -}} +{{ if .Values.proxy.inboundDiscoveryCacheUnusedTimeout -}} +- name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT + value: {{.Values.proxy.inboundDiscoveryCacheUnusedTimeout | quote}} +{{ end -}} +{{ if .Values.proxy.disableOutboundProtocolDetectTimeout -}} +- name: LINKERD2_PROXY_OUTBOUND_DETECT_TIMEOUT + value: "365d" +{{ end -}} +{{ if .Values.proxy.disableInboundProtocolDetectTimeout -}} +- name: LINKERD2_PROXY_INBOUND_DETECT_TIMEOUT + value: "365d" +{{ end -}} +- name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR + value: 0.0.0.0:{{.Values.proxy.ports.control}} +- name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR + value: 0.0.0.0:{{.Values.proxy.ports.admin}} +- name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR + value: 127.0.0.1:{{.Values.proxy.ports.outbound}} +- name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR + value: 0.0.0.0:{{.Values.proxy.ports.inbound}} +- name: LINKERD2_PROXY_INBOUND_IPS + valueFrom: + fieldRef: + fieldPath: status.podIPs +- name: LINKERD2_PROXY_INBOUND_PORTS + value: {{ .Values.proxy.podInboundPorts | quote }} +{{ if .Values.proxy.isGateway -}} +- name: LINKERD2_PROXY_INBOUND_GATEWAY_SUFFIXES + value: {{printf "svc.%s." .Values.clusterDomain}} +{{ end -}} +{{ if .Values.proxy.isIngress -}} +- name: LINKERD2_PROXY_INGRESS_MODE + value: "true" +{{ end -}} +- name: LINKERD2_PROXY_DESTINATION_PROFILE_SUFFIXES + {{- $internalDomain := printf "svc.%s." .Values.clusterDomain }} + value: {{ternary "." $internalDomain .Values.proxy.enableExternalProfiles}} +- name: LINKERD2_PROXY_INBOUND_ACCEPT_KEEPALIVE + value: 10000ms +- name: LINKERD2_PROXY_OUTBOUND_CONNECT_KEEPALIVE + value: 10000ms +{{ if .Values.proxy.opaquePorts -}} +- name: LINKERD2_PROXY_INBOUND_PORTS_DISABLE_PROTOCOL_DETECTION + value: {{.Values.proxy.opaquePorts | quote}} +{{ end -}} +- name: LINKERD2_PROXY_DESTINATION_CONTEXT + value: | + {"ns":"$(_pod_ns)", "nodeName":"$(_pod_nodeName)", "pod":"$(_pod_name)"} +- name: _pod_sa + valueFrom: + fieldRef: + fieldPath: spec.serviceAccountName +- name: _l5d_ns + value: {{.Release.Namespace}} +- name: _l5d_trustdomain + value: {{$trustDomain}} +- name: LINKERD2_PROXY_IDENTITY_DIR + value: /var/run/linkerd/identity/end-entity +- name: LINKERD2_PROXY_IDENTITY_TRUST_ANCHORS +{{- /* +Pods in the `linkerd` namespace are not injected by the proxy injector and instead obtain +the trust anchor bundle from the `linkerd-identity-trust-roots` configmap. This should not +be used in other contexts. +*/}} +{{- if .Values.proxy.loadTrustBundleFromConfigMap }} + valueFrom: + configMapKeyRef: + name: linkerd-identity-trust-roots + key: ca-bundle.crt +{{ else }} + value: | + {{- required "Please provide the identity trust anchors" .Values.identityTrustAnchorsPEM | trim | nindent 4 }} +{{ end -}} +- name: LINKERD2_PROXY_IDENTITY_TOKEN_FILE +{{- if .Values.identity.serviceAccountTokenProjection }} + value: /var/run/secrets/tokens/linkerd-identity-token +{{ else }} + value: /var/run/secrets/kubernetes.io/serviceaccount/token +{{ end -}} +- name: LINKERD2_PROXY_IDENTITY_SVC_ADDR + value: {{ternary "localhost.:8080" (printf "linkerd-identity-headless.%s.svc.%s.:8080" .Release.Namespace .Values.clusterDomain) (eq (toString .Values.proxy.component) "linkerd-identity")}} +- name: LINKERD2_PROXY_IDENTITY_LOCAL_NAME + value: $(_pod_sa).$(_pod_ns).serviceaccount.identity.{{.Release.Namespace}}.{{$trustDomain}} +- name: LINKERD2_PROXY_IDENTITY_SVC_NAME + value: linkerd-identity.{{.Release.Namespace}}.serviceaccount.identity.{{.Release.Namespace}}.{{$trustDomain}} +- name: LINKERD2_PROXY_DESTINATION_SVC_NAME + value: linkerd-destination.{{.Release.Namespace}}.serviceaccount.identity.{{.Release.Namespace}}.{{$trustDomain}} +- name: LINKERD2_PROXY_POLICY_SVC_NAME + value: linkerd-destination.{{.Release.Namespace}}.serviceaccount.identity.{{.Release.Namespace}}.{{$trustDomain}} +{{ if .Values.proxy.accessLog -}} +- name: LINKERD2_PROXY_ACCESS_LOG + value: {{.Values.proxy.accessLog | quote}} +{{ end -}} +{{ if .Values.proxy.shutdownGracePeriod -}} +- name: LINKERD2_PROXY_SHUTDOWN_GRACE_PERIOD + value: {{.Values.proxy.shutdownGracePeriod | quote}} +{{ end -}} +image: {{.Values.proxy.image.name}}:{{.Values.proxy.image.version | default .Values.linkerdVersion}} +imagePullPolicy: {{.Values.proxy.image.pullPolicy | default .Values.imagePullPolicy}} +livenessProbe: + httpGet: + path: /live + port: {{.Values.proxy.ports.admin}} + initialDelaySeconds: 10 +name: linkerd-proxy +ports: +- containerPort: {{.Values.proxy.ports.inbound}} + name: linkerd-proxy +- containerPort: {{.Values.proxy.ports.admin}} + name: linkerd-admin +readinessProbe: + httpGet: + path: /ready + port: {{.Values.proxy.ports.admin}} + initialDelaySeconds: 2 +{{- if .Values.proxy.resources }} +{{ include "partials.resources" .Values.proxy.resources }} +{{- end }} +securityContext: + allowPrivilegeEscalation: false + {{- if .Values.proxy.capabilities -}} + {{- include "partials.proxy.capabilities" . | nindent 2 -}} + {{- end }} + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: {{.Values.proxy.uid}} + seccompProfile: + type: RuntimeDefault +terminationMessagePolicy: FallbackToLogsOnError +{{- if or (.Values.proxy.await) (.Values.proxy.waitBeforeExitSeconds) }} +lifecycle: +{{- if .Values.proxy.await }} + postStart: + exec: + command: + - /usr/lib/linkerd/linkerd-await + - --timeout=2m + - --port={{.Values.proxy.ports.admin}} +{{- end }} +{{- if .Values.proxy.waitBeforeExitSeconds }} + preStop: + exec: + command: + - /bin/sleep + - {{.Values.proxy.waitBeforeExitSeconds | quote}} +{{- end }} +{{- end }} +volumeMounts: +- mountPath: /var/run/linkerd/identity/end-entity + name: linkerd-identity-end-entity +{{- if .Values.identity.serviceAccountTokenProjection }} +- mountPath: /var/run/secrets/tokens + name: linkerd-identity-token +{{- end }} +{{- if .Values.proxy.saMountPath }} +- mountPath: {{.Values.proxy.saMountPath.mountPath}} + name: {{.Values.proxy.saMountPath.name}} + readOnly: {{.Values.proxy.saMountPath.readOnly}} +{{- end -}} +{{- end }} diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_pull-secrets.tpl b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_pull-secrets.tpl new file mode 100644 index 0000000..0c9aa4f --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_pull-secrets.tpl @@ -0,0 +1,6 @@ +{{- define "partials.image-pull-secrets"}} +{{- if . }} +imagePullSecrets: +{{ toYaml . | indent 2 }} +{{- end }} +{{- end -}} diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_resources.tpl b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_resources.tpl new file mode 100644 index 0000000..1fd6789 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_resources.tpl @@ -0,0 +1,28 @@ +{{- define "partials.resources" -}} +{{- $ephemeralStorage := index . "ephemeral-storage" -}} +resources: + {{- if or (.cpu).limit (.memory).limit ($ephemeralStorage).limit }} + limits: + {{- with (.cpu).limit }} + cpu: {{. | quote}} + {{- end }} + {{- with (.memory).limit }} + memory: {{. | quote}} + {{- end }} + {{- with ($ephemeralStorage).limit }} + ephemeral-storage: {{. | quote}} + {{- end }} + {{- end }} + {{- if or (.cpu).request (.memory).request ($ephemeralStorage).request }} + requests: + {{- with (.cpu).request }} + cpu: {{. | quote}} + {{- end }} + {{- with (.memory).request }} + memory: {{. | quote}} + {{- end }} + {{- with ($ephemeralStorage).request }} + ephemeral-storage: {{. | quote}} + {{- end }} + {{- end }} +{{- end }} diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_tolerations.tpl b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_tolerations.tpl new file mode 100644 index 0000000..c2292b1 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_tolerations.tpl @@ -0,0 +1,4 @@ +{{- define "linkerd.tolerations" -}} +tolerations: +{{ toYaml .Values.tolerations | trim | indent 2 }} +{{- end -}} diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_trace.tpl b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_trace.tpl new file mode 100644 index 0000000..dee0595 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_trace.tpl @@ -0,0 +1,5 @@ +{{ define "partials.linkerd.trace" -}} +{{ if .Values.controlPlaneTracing -}} +- -trace-collector=collector.{{.Values.controlPlaneTracingNamespace}}.svc.{{.Values.clusterDomain}}:55678 +{{ end -}} +{{- end }} diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_validate.tpl b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_validate.tpl new file mode 100644 index 0000000..ba772c2 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_validate.tpl @@ -0,0 +1,19 @@ +{{- define "linkerd.webhook.validation" -}} + +{{- if and (.injectCaFrom) (.injectCaFromSecret) -}} +{{- fail "injectCaFrom and injectCaFromSecret cannot both be set" -}} +{{- end -}} + +{{- if and (or (.injectCaFrom) (.injectCaFromSecret)) (.caBundle) -}} +{{- fail "injectCaFrom or injectCaFromSecret cannot be set if providing a caBundle" -}} +{{- end -}} + +{{- if and (.externalSecret) (empty .caBundle) (empty .injectCaFrom) (empty .injectCaFromSecret) -}} +{{- fail "if externalSecret is set, then caBundle, injectCaFrom, or injectCaFromSecret must be set" -}} +{{- end }} + +{{- if and (or .injectCaFrom .injectCaFromSecret .caBundle) (not .externalSecret) -}} +{{- fail "if caBundle, injectCaFrom, or injectCaFromSecret is set, then externalSecret must be set" -}} +{{- end -}} + +{{- end -}} diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_volumes.tpl b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_volumes.tpl new file mode 100644 index 0000000..9684cf2 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/templates/_volumes.tpl @@ -0,0 +1,20 @@ +{{ define "partials.proxy.volumes.identity" -}} +emptyDir: + medium: Memory +name: linkerd-identity-end-entity +{{- end -}} + +{{ define "partials.proxyInit.volumes.xtables" -}} +emptyDir: {} +name: {{ .Values.proxyInit.xtMountPath.name }} +{{- end -}} + +{{- define "partials.proxy.volumes.service-account-token" -}} +name: linkerd-identity-token +projected: + sources: + - serviceAccountToken: + path: linkerd-identity-token + expirationSeconds: 86400 {{- /* # 24 hours */}} + audience: identity.l5d.io +{{- end -}} diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/values.yaml b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/charts/partials/values.yaml new file mode 100644 index 0000000..e69de29 diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/NOTES.txt b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/NOTES.txt new file mode 100644 index 0000000..4ff5c18 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/NOTES.txt @@ -0,0 +1,6 @@ +The linkerd-crds chart was successfully installed 🎉 + +To complete the linkerd core installation, please now proceed to install the +linkerd-control-plane chart in the {{ .Release.Namespace }} namespace. + +Looking for more? Visit https://linkerd.io/2/getting-started/ diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/gateway.networking.k8s.io_httproutes.yaml b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/gateway.networking.k8s.io_httproutes.yaml new file mode 100644 index 0000000..2a88ae2 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/gateway.networking.k8s.io_httproutes.yaml @@ -0,0 +1,4012 @@ +{{- if .Values.enableHttpRoutes }} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/1923 + gateway.networking.k8s.io/bundle-version: v0.7.1-dev + gateway.networking.k8s.io/channel: experimental + {{ include "partials.annotations.created-by" . }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + linkerd.io/control-plane-ns: {{.Release.Namespace}} + creationTimestamp: null + name: httproutes.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: HTTPRoute + listKind: HTTPRouteList + plural: httproutes + singular: httproute + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.hostnames + name: Hostnames + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + deprecated: true + deprecationWarning: The v1alpha2 version of HTTPRoute has been deprecated and + will be removed in a future release of the API. Please upgrade to v1beta1. + name: v1alpha2 + schema: + openAPIV3Schema: + description: HTTPRoute provides a way to route HTTP requests. This includes + the capability to match requests by hostname, path, header, or query param. + Filters can be used to specify additional processing steps. Backends specify + where matching requests should be routed. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of HTTPRoute. + properties: + hostnames: + description: "Hostnames defines a set of hostname that should match + against the HTTP Host header to select a HTTPRoute used to process + the request. Implementations MUST ignore any port value specified + in the HTTP Host header while performing a match and (absent of + any applicable header modification configuration) MUST forward this + header unmodified to the backend. \n Valid values for Hostnames + are determined by RFC 1123 definition of a hostname with 2 notable + exceptions: \n 1. IPs are not allowed. 2. A hostname may be prefixed + with a wildcard label (`*.`). The wildcard label must appear by + itself as the first label. \n If a hostname is specified by both + the Listener and HTTPRoute, there must be at least one intersecting + hostname for the HTTPRoute to be attached to the Listener. For example: + \n * A Listener with `test.example.com` as the hostname matches + HTTPRoutes that have either not specified any hostnames, or have + specified at least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches HTTPRoutes + that have either not specified any hostnames or have specified at + least one hostname that matches the Listener hostname. For example, + `*.example.com`, `test.example.com`, and `foo.test.example.com` + would all match. On the other hand, `example.com` and `test.example.net` + would not match. \n Hostnames that are prefixed with a wildcard + label (`*.`) are interpreted as a suffix match. That means that + a match for `*.example.com` would match both `test.example.com`, + and `foo.test.example.com`, but not `example.com`. \n If both the + Listener and HTTPRoute have specified hostnames, any HTTPRoute hostnames + that do not match the Listener hostname MUST be ignored. For example, + if a Listener specified `*.example.com`, and the HTTPRoute specified + `test.example.com` and `test.example.net`, `test.example.net` must + not be considered for a match. \n If both the Listener and HTTPRoute + have specified hostnames, and none match with the criteria above, + then the HTTPRoute is not accepted. The implementation must raise + an 'Accepted' Condition with a status of `False` in the corresponding + RouteParentStatus. \n In the event that multiple HTTPRoutes specify + intersecting hostnames (e.g. overlapping wildcard matching and exact + matching hostnames), precedence must be given to rules from the + HTTPRoute with the largest number of: \n * Characters in a matching + non-wildcard hostname. * Characters in a matching hostname. \n If + ties exist across multiple Routes, the matching precedence rules + for HTTPRouteMatches takes over. \n Support: Core" + items: + description: "Hostname is the fully qualified domain name of a network + host. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname + may be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n Hostname can be \"precise\" + which is a domain name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", which is a domain + name prefixed with a single wildcard label (e.g. `*.example.com`). + \n Note that as per RFC1035 and RFC1123, a *label* must consist + of lower case alphanumeric characters or '-', and must start and + end with an alphanumeric character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + parentRefs: + description: "ParentRefs references the resources (usually Gateways) + that a Route wants to be attached to. Note that the referenced parent + resource needs to allow this for the attachment to be complete. + For Gateways, that means the Gateway needs to allow attachment from + Routes of this kind and namespace. \n The only kind of parent resource + with \"Core\" support is Gateway. This API may be extended in the + future to support additional kinds of parent resources such as one + of the route kinds. \n It is invalid to reference an identical parent + more than once. It is valid to reference multiple distinct sections + within the same parent resource, such as 2 Listeners within a Gateway. + \n It is possible to separately reference multiple distinct objects + that may be collapsed by an implementation. For example, some implementations + may choose to merge compatible Gateway Listeners together. If that + is the case, the list of routes attached to those resources should + also be merged. \n Note that for ParentRefs that cross namespace + boundaries, there are specific rules. Cross-namespace references + are only valid if they are explicitly allowed by something in the + namespace they are referring to. For example, Gateway has the AllowedRoutes + field, and ReferenceGrant provides a generic way to enable any other + kind of cross-namespace reference." + items: + description: "ParentReference identifies an API object (usually + a Gateway) that can be considered a parent of this resource (usually + a route). The only kind of parent resource with \"Core\" support + is Gateway. This API may be extended in the future to support + additional kinds of parent resources, such as HTTPRoute. \n The + API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid." + properties: + group: + default: gateway.networking.k8s.io + description: "Group is the group of the referent. When unspecified, + \"gateway.networking.k8s.io\" is inferred. To set the core + API group (such as for a \"Service\" kind referent), Group + must be explicitly set to \"\" (empty string). \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: Core + (Gateway) \n Support: Implementation-specific (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. When + unspecified, this refers to the local namespace of the Route. + \n Note that there are specific rules for ParentRefs which + cross namespace boundaries. Cross-namespace references are + only valid if they are explicitly allowed by something in + the namespace they are referring to. For example: Gateway + has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: "Port is the network port this Route targets. It + can be interpreted differently based on the type of parent + resource. \n When the parent resource is a Gateway, this targets + all listeners listening on the specified port that also support + this kind of Route(and select this Route). It's not recommended + to set `Port` unless the networking behaviors specified in + a Route must apply to a specific port as opposed to a listener(s) + whose port(s) may be changed. When both Port and SectionName + are specified, the name and port of the selected listener + must match both specified values. \n Implementations MAY choose + to support other parent resources. Implementations supporting + other types of parent resources MUST clearly document how/if + Port is interpreted. \n For the purpose of status, an attachment + is considered successful as long as the parent resource accepts + it partially. For example, Gateway listeners can restrict + which Routes can attach to them by Route kind, namespace, + or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this + Route, the Route MUST be considered detached from the Gateway. + \n Support: Extended \n " + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: "SectionName is the name of a section within the + target resource. In the following resources, SectionName is + interpreted as the following: \n * Gateway: Listener Name. + When both Port (experimental) and SectionName are specified, + the name and port of the selected listener must match both + specified values. \n Implementations MAY choose to support + attaching Routes to other resources. If that is the case, + they MUST clearly document how SectionName is interpreted. + \n When unspecified (empty string), this will reference the + entire resource. For the purpose of status, an attachment + is considered successful if at least one section in the parent + resource accepts it. For example, Gateway listeners can restrict + which Routes can attach to them by Route kind, namespace, + or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this + Route, the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + rules: + default: + - matches: + - path: + type: PathPrefix + value: / + description: Rules are a list of HTTP matchers, filters and actions. + items: + description: HTTPRouteRule defines semantics for matching an HTTP + request based on conditions (matches), processing it (filters), + and forwarding the request to an API object (backendRefs). + properties: + backendRefs: + description: "BackendRefs defines the backend(s) where matching + requests should be sent. \n Failure behavior here depends + on how many BackendRefs are specified and how many are invalid. + \n If *all* entries in BackendRefs are invalid, and there + are also no filters specified in this route rule, *all* traffic + which matches this rule MUST receive a 500 status code. \n + See the HTTPBackendRef definition for the rules about what + makes a single HTTPBackendRef invalid. \n When a HTTPBackendRef + is invalid, 500 status codes MUST be returned for requests + that would have otherwise been routed to an invalid backend. + If multiple backends are specified, and some are invalid, + the proportion of requests that would otherwise have been + routed to an invalid backend MUST receive a 500 status code. + \n For example, if two backends are specified with equal weights, + and one is invalid, 50 percent of traffic must receive a 500. + Implementations may choose how that 50 percent is determined. + \n Support: Core for Kubernetes Service \n Support: Extended + for Kubernetes ServiceImport \n Support: Implementation-specific + for any other resource \n Support for weight: Core" + items: + description: HTTPBackendRef defines how a HTTPRoute should + forward an HTTP request. + properties: + filters: + description: "Filters defined at this level should be + executed if and only if the request is being forwarded + to the backend defined here. \n Support: Implementation-specific + (For broader support of filters, use the Filters field + in HTTPRouteRule.)" + items: + description: HTTPRouteFilter defines processing steps + that must be completed during the request or response + lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway + implementations. Some examples include request or + response modification, implementing authentication + strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type + of the filter. + properties: + extensionRef: + description: "ExtensionRef is an optional, implementation-specific + extension to the \"filter\" behavior. For example, + resource \"myroutefilter\" in group \"networking.example.net\"). + ExtensionRef MUST NOT be used for core and extended + filters. \n Support: Implementation-specific" + properties: + group: + description: Group is the group of the referent. + For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API + group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For + example "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema + for a filter that modifies request headers. \n + Support: Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: "RequestMirror defines a schema for + a filter that mirrors requests. Requests are sent + to the specified destination, but responses from + that destination are ignored. \n Support: Extended" + properties: + backendRef: + description: "BackendRef references a resource + where mirrored requests are sent. \n If the + referent cannot be found, this BackendRef + is invalid and must be dropped from the Gateway. + The controller must ensure the \"ResolvedRefs\" + condition on the Route status is set to `status: + False` and not configure this backend in the + underlying implementation. \n If there is + a cross-namespace reference to an *existing* + object that is not allowed by a ReferenceGrant, + the controller must ensure the \"ResolvedRefs\" + \ condition on the Route is set to `status: + False`, with the \"RefNotPermitted\" reason + and not configure this backend in the underlying + implementation. \n In either error case, the + Message of the `ResolvedRefs` Condition should + be used to provide more detail about the problem. + \n Support: Extended for Kubernetes Service + \n Support: Implementation-specific for any + other resource" + properties: + group: + default: "" + description: Group is the group of the referent. + For example, "gateway.networking.k8s.io". + When unspecified or empty string, core + API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: "Kind is the Kubernetes resource + kind of the referent. For example \"Service\". + \n Defaults to \"Service\" when not specified. + \n ExternalName services can refer to + CNAME DNS records that may live outside + of the cluster and as such are difficult + to reason about in terms of conformance. + They also may not be safe to forward to + (see CVE-2021-25740 for more information). + Implementations SHOULD NOT support ExternalName + Services. \n Support: Core (Services with + a type other than ExternalName) \n Support: + Implementation-specific (Services with + type ExternalName)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace + of the backend. When unspecified, the + local namespace is inferred. \n Note that + when a namespace different than the local + namespace is specified, a ReferenceGrant + object is required in the referent namespace + to allow that namespace's owner to accept + the reference. See the ReferenceGrant + documentation for details. \n Support: + Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: Port specifies the destination + port number to use for this resource. + Port is required when the referent is + a Kubernetes Service. In this case, the + port number is the service port number, + not the target port. For other resources, + destination port might be derived from + the referent resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + required: + - backendRef + type: object + requestRedirect: + description: "RequestRedirect defines a schema for + a filter that responds to the request with an + HTTP redirection. \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be + used in the value of the `Location` header + in the response. When empty, the hostname + in the `Host` header of the request is used. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to + modify the path of the incoming request. The + modified path is then used to construct the + `Location` header. When empty, the request + path is used as-is. \n Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in + the value of the `Location` header in the + response. \n If no port is specified, the + redirect port MUST be derived using the following + rules: \n * If redirect scheme is not-empty, + the redirect port MUST be the well-known port + associated with the redirect scheme. Specifically + \"http\" to port 80 and \"https\" to port + 443. If the redirect scheme does not have + a well-known port, the listener port of the + Gateway SHOULD be used. * If redirect scheme + is empty, the redirect port MUST be the Gateway + Listener port. \n Implementations SHOULD NOT + add the port number in the 'Location' header + in the following cases: \n * A Location header + that will use HTTP (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 80. * A Location header that + will use HTTPS (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 443. \n Support: Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used + in the value of the `Location` header in the + response. When empty, the scheme of the request + is used. \n Scheme redirects can affect the + port of the redirect, for more information, + refer to the documentation for the port field + of this filter. \n Note that values may be + added to this enum, implementations must ensure + that unknown values will not cause a crash. + \n Unknown values here must result in the + implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`. \n Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status + code to be used in response. \n Note that + values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result + in the implementation setting the Accepted + Condition for the Route to `status: False`, + with a Reason of `UnsupportedValue`. \n Support: + Core" + enum: + - 301 + - 302 + type: integer + type: object + responseHeaderModifier: + description: "ResponseHeaderModifier defines a schema + for a filter that modifies response headers. \n + Support: Extended" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: "Type identifies the type of filter + to apply. As with other API fields, types are + classified into three conformance levels: \n - + Core: Filter types and their corresponding configuration + defined by \"Support: Core\" in this package, + e.g. \"RequestHeaderModifier\". All implementations + must support core filters. \n - Extended: Filter + types and their corresponding configuration defined + by \"Support: Extended\" in this package, e.g. + \"RequestMirror\". Implementers are encouraged + to support extended filters. \n - Implementation-specific: + Filters that are defined and supported by specific + vendors. In the future, filters showing convergence + in behavior across multiple implementations will + be considered for inclusion in extended or core + conformance levels. Filter-specific configuration + for such filters is specified using the ExtensionRef + field. `Type` should be set to \"ExtensionRef\" + for custom filters. \n Implementers are encouraged + to define custom implementation types to extend + the core API with implementation-specific behavior. + \n If a reference to a custom filter type cannot + be resolved, the filter MUST NOT be skipped. Instead, + requests that would have been processed by that + filter MUST receive a HTTP error response. \n + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result in + the implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestMirror + - RequestRedirect + - URLRewrite + - ExtensionRef + type: string + urlRewrite: + description: "URLRewrite defines a schema for a + filter that modifies a request during forwarding. + \n Support: Extended" + properties: + hostname: + description: "Hostname is the value to be used + to replace the Host header value during forwarding. + \n Support: Extended" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines a path rewrite. \n + Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + type: object + required: + - type + type: object + maxItems: 16 + type: array + group: + default: "" + description: Group is the group of the referent. For example, + "gateway.networking.k8s.io". When unspecified or empty + string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: "Kind is the Kubernetes resource kind of + the referent. For example \"Service\". \n Defaults to + \"Service\" when not specified. \n ExternalName services + can refer to CNAME DNS records that may live outside + of the cluster and as such are difficult to reason about + in terms of conformance. They also may not be safe to + forward to (see CVE-2021-25740 for more information). + Implementations SHOULD NOT support ExternalName Services. + \n Support: Core (Services with a type other than ExternalName) + \n Support: Implementation-specific (Services with type + ExternalName)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the backend. + When unspecified, the local namespace is inferred. \n + Note that when a namespace different than the local + namespace is specified, a ReferenceGrant object is required + in the referent namespace to allow that namespace's + owner to accept the reference. See the ReferenceGrant + documentation for details. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: Port specifies the destination port number + to use for this resource. Port is required when the + referent is a Kubernetes Service. In this case, the + port number is the service port number, not the target + port. For other resources, destination port might be + derived from the referent resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: "Weight specifies the proportion of requests + forwarded to the referenced backend. This is computed + as weight/(sum of all weights in this BackendRefs list). + For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision + an implementation supports. Weight is not a percentage + and the sum of weights does not need to equal 100. \n + If only one backend is specified and it has a weight + greater than 0, 100% of the traffic is forwarded to + that backend. If weight is set to 0, no traffic should + be forwarded for this entry. If unspecified, weight + defaults to 1. \n Support for this field varies based + on the context where used." + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + maxItems: 16 + type: array + filters: + description: "Filters define the filters that are applied to + requests that match this rule. \n The effects of ordering + of multiple behaviors are currently unspecified. This can + change in the future based on feedback during the alpha stage. + \n Conformance-levels at this level are defined based on the + type of filter: \n - ALL core filters MUST be supported by + all implementations. - Implementers are encouraged to support + extended filters. - Implementation-specific custom filters + have no API guarantees across implementations. \n Specifying + a core filter multiple times has unspecified or implementation-specific + conformance. \n All filters are expected to be compatible + with each other except for the URLRewrite and RequestRedirect + filters, which may not be combined. If an implementation can + not support other combinations of filters, they must clearly + document that limitation. In all cases where incompatible + or unsupported filters are specified, implementations MUST + add a warning condition to status. \n Support: Core" + items: + description: HTTPRouteFilter defines processing steps that + must be completed during the request or response lifecycle. + HTTPRouteFilters are meant as an extension point to express + processing that may be done in Gateway implementations. + Some examples include request or response modification, + implementing authentication strategies, rate-limiting, and + traffic shaping. API guarantee/conformance is defined based + on the type of the filter. + properties: + extensionRef: + description: "ExtensionRef is an optional, implementation-specific + extension to the \"filter\" behavior. For example, + resource \"myroutefilter\" in group \"networking.example.net\"). + ExtensionRef MUST NOT be used for core and extended + filters. \n Support: Implementation-specific" + properties: + group: + description: Group is the group of the referent. For + example, "gateway.networking.k8s.io". When unspecified + or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For example + "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema for + a filter that modifies request headers. \n Support: + Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It appends + to any existing values associated with the header + name. \n Input: GET /foo HTTP/1.1 my-header: foo + \n Config: add: - name: \"my-header\" value: \"bar,baz\" + \n Output: GET /foo HTTP/1.1 my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from the + HTTP request before the action. The value of Remove + is a list of HTTP header names. Note that the header + names are case-insensitive (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo my-header2: + bar my-header3: baz \n Config: remove: [\"my-header1\", + \"my-header3\"] \n Output: GET /foo HTTP/1.1 my-header2: + bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with the + given header (name, value) before the action. \n + Input: GET /foo HTTP/1.1 my-header: foo \n Config: + set: - name: \"my-header\" value: \"bar\" \n Output: + GET /foo HTTP/1.1 my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: "RequestMirror defines a schema for a filter + that mirrors requests. Requests are sent to the specified + destination, but responses from that destination are + ignored. \n Support: Extended" + properties: + backendRef: + description: "BackendRef references a resource where + mirrored requests are sent. \n If the referent cannot + be found, this BackendRef is invalid and must be + dropped from the Gateway. The controller must ensure + the \"ResolvedRefs\" condition on the Route status + is set to `status: False` and not configure this + backend in the underlying implementation. \n If + there is a cross-namespace reference to an *existing* + object that is not allowed by a ReferenceGrant, + the controller must ensure the \"ResolvedRefs\" + \ condition on the Route is set to `status: False`, + with the \"RefNotPermitted\" reason and not configure + this backend in the underlying implementation. \n + In either error case, the Message of the `ResolvedRefs` + Condition should be used to provide more detail + about the problem. \n Support: Extended for Kubernetes + Service \n Support: Implementation-specific for + any other resource" + properties: + group: + default: "" + description: Group is the group of the referent. + For example, "gateway.networking.k8s.io". When + unspecified or empty string, core API group + is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: "Kind is the Kubernetes resource + kind of the referent. For example \"Service\". + \n Defaults to \"Service\" when not specified. + \n ExternalName services can refer to CNAME + DNS records that may live outside of the cluster + and as such are difficult to reason about in + terms of conformance. They also may not be safe + to forward to (see CVE-2021-25740 for more information). + Implementations SHOULD NOT support ExternalName + Services. \n Support: Core (Services with a + type other than ExternalName) \n Support: Implementation-specific + (Services with type ExternalName)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the + backend. When unspecified, the local namespace + is inferred. \n Note that when a namespace different + than the local namespace is specified, a ReferenceGrant + object is required in the referent namespace + to allow that namespace's owner to accept the + reference. See the ReferenceGrant documentation + for details. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: Port specifies the destination port + number to use for this resource. Port is required + when the referent is a Kubernetes Service. In + this case, the port number is the service port + number, not the target port. For other resources, + destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + required: + - backendRef + type: object + requestRedirect: + description: "RequestRedirect defines a schema for a filter + that responds to the request with an HTTP redirection. + \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be used + in the value of the `Location` header in the response. + When empty, the hostname in the `Host` header of + the request is used. \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to modify + the path of the incoming request. The modified path + is then used to construct the `Location` header. + When empty, the request path is used as-is. \n Support: + Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the value + with which to replace the full path of a request + during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies the + value with which to replace the prefix match + of a request during a rewrite or redirect. For + example, a request to \"/foo/bar\" with a prefix + match of \"/foo\" and a ReplacePrefixMatch of + \"/xyz\" would be modified to \"/xyz/bar\". + \n Note that this matches the behavior of the + PathPrefix match type. This matches full path + elements. A path element refers to the list + of labels in the path split by the `/` separator. + When specified, a trailing `/` is ignored. For + example, the paths `/abc`, `/abc/`, and `/abc/def` + would all match the prefix `/abc`, but the path + `/abcd` would not. \n Request Path | Prefix + Match | Replace Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | /xyz/ + \ | /xyz/bar /foo/bar | /foo/ | + /xyz | /xyz/bar /foo/bar | /foo/ + \ | /xyz/ | /xyz/bar /foo | + /foo | /xyz | /xyz /foo/ | + /foo | /xyz | /xyz/ /foo/bar + \ | /foo | | /bar + /foo/ | /foo | + | / /foo | /foo | + | / /foo/ | /foo | / | + / /foo | /foo | / | + /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path modifier. + Additional types may be added in a future release + of the API. \n Note that values may be added + to this enum, implementations must ensure that + unknown values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the Route + to `status: False`, with a Reason of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in the value + of the `Location` header in the response. \n If + no port is specified, the redirect port MUST be + derived using the following rules: \n * If redirect + scheme is not-empty, the redirect port MUST be the + well-known port associated with the redirect scheme. + Specifically \"http\" to port 80 and \"https\" to + port 443. If the redirect scheme does not have a + well-known port, the listener port of the Gateway + SHOULD be used. * If redirect scheme is empty, the + redirect port MUST be the Gateway Listener port. + \n Implementations SHOULD NOT add the port number + in the 'Location' header in the following cases: + \n * A Location header that will use HTTP (whether + that is determined via the Listener protocol or + the Scheme field) _and_ use port 80. * A Location + header that will use HTTPS (whether that is determined + via the Listener protocol or the Scheme field) _and_ + use port 443. \n Support: Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used in the + value of the `Location` header in the response. + When empty, the scheme of the request is used. \n + Scheme redirects can affect the port of the redirect, + for more information, refer to the documentation + for the port field of this filter. \n Note that + values may be added to this enum, implementations + must ensure that unknown values will not cause a + crash. \n Unknown values here must result in the + implementation setting the Accepted Condition for + the Route to `status: False`, with a Reason of `UnsupportedValue`. + \n Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status code to + be used in response. \n Note that values may be + added to this enum, implementations must ensure + that unknown values will not cause a crash. \n Unknown + values here must result in the implementation setting + the Accepted Condition for the Route to `status: + False`, with a Reason of `UnsupportedValue`. \n + Support: Core" + enum: + - 301 + - 302 + type: integer + type: object + responseHeaderModifier: + description: "ResponseHeaderModifier defines a schema + for a filter that modifies response headers. \n Support: + Extended" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It appends + to any existing values associated with the header + name. \n Input: GET /foo HTTP/1.1 my-header: foo + \n Config: add: - name: \"my-header\" value: \"bar,baz\" + \n Output: GET /foo HTTP/1.1 my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from the + HTTP request before the action. The value of Remove + is a list of HTTP header names. Note that the header + names are case-insensitive (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo my-header2: + bar my-header3: baz \n Config: remove: [\"my-header1\", + \"my-header3\"] \n Output: GET /foo HTTP/1.1 my-header2: + bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with the + given header (name, value) before the action. \n + Input: GET /foo HTTP/1.1 my-header: foo \n Config: + set: - name: \"my-header\" value: \"bar\" \n Output: + GET /foo HTTP/1.1 my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: "Type identifies the type of filter to apply. + As with other API fields, types are classified into + three conformance levels: \n - Core: Filter types and + their corresponding configuration defined by \"Support: + Core\" in this package, e.g. \"RequestHeaderModifier\". + All implementations must support core filters. \n - + Extended: Filter types and their corresponding configuration + defined by \"Support: Extended\" in this package, e.g. + \"RequestMirror\". Implementers are encouraged to support + extended filters. \n - Implementation-specific: Filters + that are defined and supported by specific vendors. + In the future, filters showing convergence in behavior + across multiple implementations will be considered for + inclusion in extended or core conformance levels. Filter-specific + configuration for such filters is specified using the + ExtensionRef field. `Type` should be set to \"ExtensionRef\" + for custom filters. \n Implementers are encouraged to + define custom implementation types to extend the core + API with implementation-specific behavior. \n If a reference + to a custom filter type cannot be resolved, the filter + MUST NOT be skipped. Instead, requests that would have + been processed by that filter MUST receive a HTTP error + response. \n Note that values may be added to this enum, + implementations must ensure that unknown values will + not cause a crash. \n Unknown values here must result + in the implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason of `UnsupportedValue`." + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestMirror + - RequestRedirect + - URLRewrite + - ExtensionRef + type: string + urlRewrite: + description: "URLRewrite defines a schema for a filter + that modifies a request during forwarding. \n Support: + Extended" + properties: + hostname: + description: "Hostname is the value to be used to + replace the Host header value during forwarding. + \n Support: Extended" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines a path rewrite. \n Support: + Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the value + with which to replace the full path of a request + during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies the + value with which to replace the prefix match + of a request during a rewrite or redirect. For + example, a request to \"/foo/bar\" with a prefix + match of \"/foo\" and a ReplacePrefixMatch of + \"/xyz\" would be modified to \"/xyz/bar\". + \n Note that this matches the behavior of the + PathPrefix match type. This matches full path + elements. A path element refers to the list + of labels in the path split by the `/` separator. + When specified, a trailing `/` is ignored. For + example, the paths `/abc`, `/abc/`, and `/abc/def` + would all match the prefix `/abc`, but the path + `/abcd` would not. \n Request Path | Prefix + Match | Replace Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | /xyz/ + \ | /xyz/bar /foo/bar | /foo/ | + /xyz | /xyz/bar /foo/bar | /foo/ + \ | /xyz/ | /xyz/bar /foo | + /foo | /xyz | /xyz /foo/ | + /foo | /xyz | /xyz/ /foo/bar + \ | /foo | | /bar + /foo/ | /foo | + | / /foo | /foo | + | / /foo/ | /foo | / | + / /foo | /foo | / | + /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path modifier. + Additional types may be added in a future release + of the API. \n Note that values may be added + to this enum, implementations must ensure that + unknown values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the Route + to `status: False`, with a Reason of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + type: object + required: + - type + type: object + maxItems: 16 + type: array + matches: + default: + - path: + type: PathPrefix + value: / + description: "Matches define conditions used for matching the + rule against incoming HTTP requests. Each match is independent, + i.e. this rule will be matched if **any** one of the matches + is satisfied. \n For example, take the following matches configuration: + \n ``` matches: - path: value: \"/foo\" headers: - name: \"version\" + value: \"v2\" - path: value: \"/v2/foo\" ``` \n For a request + to match against this rule, a request must satisfy EITHER + of the two conditions: \n - path prefixed with `/foo` AND + contains the header `version: v2` - path prefix of `/v2/foo` + \n See the documentation for HTTPRouteMatch on how to specify + multiple match conditions that should be ANDed together. \n + If no matches are specified, the default is a prefix path + match on \"/\", which has the effect of matching every HTTP + request. \n Proxy or Load Balancer routing configuration generated + from HTTPRoutes MUST prioritize matches based on the following + criteria, continuing on ties. Across all rules specified on + applicable Routes, precedence must be given to the match having: + \n * \"Exact\" path match. * \"Prefix\" path match with largest + number of characters. * Method match. * Largest number of + header matches. * Largest number of query param matches. \n + Note: The precedence of RegularExpression path matches are + implementation-specific. \n If ties still exist across multiple + Routes, matching precedence MUST be determined in order of + the following criteria, continuing on ties: \n * The oldest + Route based on creation timestamp. * The Route appearing first + in alphabetical order by \"{namespace}/{name}\". \n If ties + still exist within an HTTPRoute, matching precedence MUST + be granted to the FIRST matching rule (in list order) with + a match meeting the above criteria. \n When no rules matching + a request have been successfully attached to the parent a + request is coming from, a HTTP 404 status code MUST be returned." + items: + description: "HTTPRouteMatch defines the predicate used to + match requests to a given action. Multiple match types are + ANDed together, i.e. the match will evaluate to true only + if all conditions are satisfied. \n For example, the match + below will match a HTTP request only if its path starts + with `/foo` AND it contains the `version: v1` header: \n + ``` match: \n path: value: \"/foo\" headers: - name: \"version\" + value \"v1\" \n ```" + properties: + headers: + description: Headers specifies HTTP request header matchers. + Multiple match values are ANDed together, meaning, a + request must match all the specified headers to select + the route. + items: + description: HTTPHeaderMatch describes how to select + a HTTP route by matching HTTP request headers. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case insensitive. + (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent header + names, only the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST be + ignored. Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered equivalent. + \n When a header is repeated in an HTTP request, + it is implementation-specific behavior as to how + this is represented. Generally, proxies should + follow the guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, with special + handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the header. \n Support: Core (Exact) + \n Support: Implementation-specific (RegularExpression) + \n Since RegularExpression HeaderMatchType has + implementation-specific conformance, implementations + can support POSIX, PCRE or any other dialects + of regular expressions. Please read the implementation's + documentation to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP Header to + be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. When + specified, this route will be matched only if the request + has the specified method. \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path matcher. + If this field is not specified, a default prefix match + on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match against + the path Value. \n Support: Core (Exact, PathPrefix) + \n Support: Implementation-specific (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match against. + maxLength: 1024 + type: string + type: object + queryParams: + description: "QueryParams specifies HTTP query parameter + matchers. Multiple match values are ANDed together, + meaning, a request must match all the specified query + parameters to select the route. \n Support: Extended" + items: + description: HTTPQueryParamMatch describes how to select + a HTTP route by matching HTTP query parameters. + properties: + name: + description: "Name is the name of the HTTP query + param to be matched. This must be an exact string + match. (See https://tools.ietf.org/html/rfc7230#section-2.7.3). + \n If multiple entries specify equivalent query + param names, only the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent query param name MUST + be ignored. \n If a query param is repeated in + an HTTP request, the behavior is purposely left + undefined, since different data planes have different + capabilities. However, it is *recommended* that + implementations should match against the first + value of the param if the data plane supports + it, as this behavior is expected in other load + balancing contexts outside of the Gateway API. + \n Users SHOULD NOT route traffic based on repeated + query params to guard themselves against potential + differences in the implementations." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the query parameter. \n Support: + Extended (Exact) \n Support: Implementation-specific + (RegularExpression) \n Since RegularExpression + QueryParamMatchType has Implementation-specific + conformance, implementations can support POSIX, + PCRE or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP query param + to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + maxItems: 8 + type: array + type: object + maxItems: 16 + type: array + type: object + status: + description: Status defines the current state of HTTPRoute. + properties: + parents: + description: "Parents is a list of parent resources (usually Gateways) + that are associated with the route, and the status of the route + with respect to each parent. When this route attaches to a parent, + the controller that manages the parent must add an entry to this + list when the controller first sees the route and should update + the entry as appropriate when the route or gateway is modified. + \n Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this + API can only populate Route status for the Gateways/parent resources + they are responsible for. \n A maximum of 32 Gateways will be represented + in this list. An empty list means the route has not been attached + to any Gateway." + items: + description: RouteParentStatus describes the status of a route with + respect to an associated Parent. + properties: + conditions: + description: "Conditions describes the status of the route with + respect to the Gateway. Note that the route's availability + is also subject to the Gateway's own status conditions and + listener status. \n If the Route's ParentRef specifies an + existing Gateway that supports Routes of this kind AND that + Gateway's controller has sufficient access, then that Gateway's + controller MUST set the \"Accepted\" condition on the Route, + to indicate whether the route has been accepted or rejected + by the Gateway, and why. \n A Route MUST be considered \"Accepted\" + if at least one of the Route's rules is implemented by the + Gateway. \n There are a number of cases where the \"Accepted\" + condition may not be set due to lack of controller visibility, + that includes when: \n * The Route refers to a non-existent + parent. * The Route is of a type that the controller does + not support. * The Route is in a namespace the controller + does not have access to." + items: + description: "Condition contains details for one aspect of + the current state of this API Resource. --- This struct + is intended for direct use as an array at the field path + .status.conditions. For example, \n type FooStatus struct{ + // Represents the observations of a foo's current state. + // Known .status.conditions.type are: \"Available\", \"Progressing\", + and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields + }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should + be when the underlying condition changed. If that is + not known, then using the time when the API field changed + is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, + if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the + current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier + indicating the reason for the condition's last transition. + Producers of specific condition types may define expected + values and meanings for this field, and whether the + values are considered a guaranteed API. The value should + be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across + resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability + to deconflict is important. The regex it matches is + (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: "ControllerName is a domain/path string that indicates + the name of the controller that wrote this status. This corresponds + with the controllerName field on GatewayClass. \n Example: + \"example.net/gateway-controller\". \n The format of this + field is DOMAIN \"/\" PATH, where DOMAIN and PATH are valid + Kubernetes names (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + \n Controllers MUST populate this field when writing status. + Controllers should ensure that entries to status populated + with their ControllerName are cleaned up when they are no + longer necessary." + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: ParentRef corresponds with a ParentRef in the spec + that this RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: "Group is the group of the referent. When unspecified, + \"gateway.networking.k8s.io\" is inferred. To set the + core API group (such as for a \"Service\" kind referent), + Group must be explicitly set to \"\" (empty string). \n + Support: Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: + Core (Gateway) \n Support: Implementation-specific (Other + Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. + When unspecified, this refers to the local namespace of + the Route. \n Note that there are specific rules for ParentRefs + which cross namespace boundaries. Cross-namespace references + are only valid if they are explicitly allowed by something + in the namespace they are referring to. For example: Gateway + has the AllowedRoutes field, and ReferenceGrant provides + a generic way to enable any other kind of cross-namespace + reference. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: "Port is the network port this Route targets. + It can be interpreted differently based on the type of + parent resource. \n When the parent resource is a Gateway, + this targets all listeners listening on the specified + port that also support this kind of Route(and select this + Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to + a specific port as opposed to a listener(s) whose port(s) + may be changed. When both Port and SectionName are specified, + the name and port of the selected listener must match + both specified values. \n Implementations MAY choose to + support other parent resources. Implementations supporting + other types of parent resources MUST clearly document + how/if Port is interpreted. \n For the purpose of status, + an attachment is considered successful as long as the + parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them + by Route kind, namespace, or hostname. If 1 of 2 Gateway + listeners accept attachment from the referencing Route, + the Route MUST be considered successfully attached. If + no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + \n Support: Extended \n " + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: "SectionName is the name of a section within + the target resource. In the following resources, SectionName + is interpreted as the following: \n * Gateway: Listener + Name. When both Port (experimental) and SectionName are + specified, the name and port of the selected listener + must match both specified values. \n Implementations MAY + choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName + is interpreted. \n When unspecified (empty string), this + will reference the entire resource. For the purpose of + status, an attachment is considered successful if at least + one section in the parent resource accepts it. For example, + Gateway listeners can restrict which Routes can attach + to them by Route kind, namespace, or hostname. If 1 of + 2 Gateway listeners accept attachment from the referencing + Route, the Route MUST be considered successfully attached. + If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + required: + - parents + type: object + required: + - spec + type: object + served: false + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.hostnames + name: Hostnames + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: HTTPRoute provides a way to route HTTP requests. This includes + the capability to match requests by hostname, path, header, or query param. + Filters can be used to specify additional processing steps. Backends specify + where matching requests should be routed. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of HTTPRoute. + properties: + hostnames: + description: "Hostnames defines a set of hostname that should match + against the HTTP Host header to select a HTTPRoute used to process + the request. Implementations MUST ignore any port value specified + in the HTTP Host header while performing a match and (absent of + any applicable header modification configuration) MUST forward this + header unmodified to the backend. \n Valid values for Hostnames + are determined by RFC 1123 definition of a hostname with 2 notable + exceptions: \n 1. IPs are not allowed. 2. A hostname may be prefixed + with a wildcard label (`*.`). The wildcard label must appear by + itself as the first label. \n If a hostname is specified by both + the Listener and HTTPRoute, there must be at least one intersecting + hostname for the HTTPRoute to be attached to the Listener. For example: + \n * A Listener with `test.example.com` as the hostname matches + HTTPRoutes that have either not specified any hostnames, or have + specified at least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches HTTPRoutes + that have either not specified any hostnames or have specified at + least one hostname that matches the Listener hostname. For example, + `*.example.com`, `test.example.com`, and `foo.test.example.com` + would all match. On the other hand, `example.com` and `test.example.net` + would not match. \n Hostnames that are prefixed with a wildcard + label (`*.`) are interpreted as a suffix match. That means that + a match for `*.example.com` would match both `test.example.com`, + and `foo.test.example.com`, but not `example.com`. \n If both the + Listener and HTTPRoute have specified hostnames, any HTTPRoute hostnames + that do not match the Listener hostname MUST be ignored. For example, + if a Listener specified `*.example.com`, and the HTTPRoute specified + `test.example.com` and `test.example.net`, `test.example.net` must + not be considered for a match. \n If both the Listener and HTTPRoute + have specified hostnames, and none match with the criteria above, + then the HTTPRoute is not accepted. The implementation must raise + an 'Accepted' Condition with a status of `False` in the corresponding + RouteParentStatus. \n In the event that multiple HTTPRoutes specify + intersecting hostnames (e.g. overlapping wildcard matching and exact + matching hostnames), precedence must be given to rules from the + HTTPRoute with the largest number of: \n * Characters in a matching + non-wildcard hostname. * Characters in a matching hostname. \n If + ties exist across multiple Routes, the matching precedence rules + for HTTPRouteMatches takes over. \n Support: Core" + items: + description: "Hostname is the fully qualified domain name of a network + host. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname + may be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n Hostname can be \"precise\" + which is a domain name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", which is a domain + name prefixed with a single wildcard label (e.g. `*.example.com`). + \n Note that as per RFC1035 and RFC1123, a *label* must consist + of lower case alphanumeric characters or '-', and must start and + end with an alphanumeric character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + parentRefs: + description: "ParentRefs references the resources (usually Gateways) + that a Route wants to be attached to. Note that the referenced parent + resource needs to allow this for the attachment to be complete. + For Gateways, that means the Gateway needs to allow attachment from + Routes of this kind and namespace. \n The only kind of parent resource + with \"Core\" support is Gateway. This API may be extended in the + future to support additional kinds of parent resources such as one + of the route kinds. \n It is invalid to reference an identical parent + more than once. It is valid to reference multiple distinct sections + within the same parent resource, such as 2 Listeners within a Gateway. + \n It is possible to separately reference multiple distinct objects + that may be collapsed by an implementation. For example, some implementations + may choose to merge compatible Gateway Listeners together. If that + is the case, the list of routes attached to those resources should + also be merged. \n Note that for ParentRefs that cross namespace + boundaries, there are specific rules. Cross-namespace references + are only valid if they are explicitly allowed by something in the + namespace they are referring to. For example, Gateway has the AllowedRoutes + field, and ReferenceGrant provides a generic way to enable any other + kind of cross-namespace reference." + items: + description: "ParentReference identifies an API object (usually + a Gateway) that can be considered a parent of this resource (usually + a route). The only kind of parent resource with \"Core\" support + is Gateway. This API may be extended in the future to support + additional kinds of parent resources, such as HTTPRoute. \n The + API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid." + properties: + group: + default: gateway.networking.k8s.io + description: "Group is the group of the referent. When unspecified, + \"gateway.networking.k8s.io\" is inferred. To set the core + API group (such as for a \"Service\" kind referent), Group + must be explicitly set to \"\" (empty string). \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: Core + (Gateway) \n Support: Implementation-specific (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. When + unspecified, this refers to the local namespace of the Route. + \n Note that there are specific rules for ParentRefs which + cross namespace boundaries. Cross-namespace references are + only valid if they are explicitly allowed by something in + the namespace they are referring to. For example: Gateway + has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: "Port is the network port this Route targets. It + can be interpreted differently based on the type of parent + resource. \n When the parent resource is a Gateway, this targets + all listeners listening on the specified port that also support + this kind of Route(and select this Route). It's not recommended + to set `Port` unless the networking behaviors specified in + a Route must apply to a specific port as opposed to a listener(s) + whose port(s) may be changed. When both Port and SectionName + are specified, the name and port of the selected listener + must match both specified values. \n Implementations MAY choose + to support other parent resources. Implementations supporting + other types of parent resources MUST clearly document how/if + Port is interpreted. \n For the purpose of status, an attachment + is considered successful as long as the parent resource accepts + it partially. For example, Gateway listeners can restrict + which Routes can attach to them by Route kind, namespace, + or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this + Route, the Route MUST be considered detached from the Gateway. + \n Support: Extended \n " + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: "SectionName is the name of a section within the + target resource. In the following resources, SectionName is + interpreted as the following: \n * Gateway: Listener Name. + When both Port (experimental) and SectionName are specified, + the name and port of the selected listener must match both + specified values. \n Implementations MAY choose to support + attaching Routes to other resources. If that is the case, + they MUST clearly document how SectionName is interpreted. + \n When unspecified (empty string), this will reference the + entire resource. For the purpose of status, an attachment + is considered successful if at least one section in the parent + resource accepts it. For example, Gateway listeners can restrict + which Routes can attach to them by Route kind, namespace, + or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this + Route, the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + rules: + default: + - matches: + - path: + type: PathPrefix + value: / + description: Rules are a list of HTTP matchers, filters and actions. + items: + description: HTTPRouteRule defines semantics for matching an HTTP + request based on conditions (matches), processing it (filters), + and forwarding the request to an API object (backendRefs). + properties: + backendRefs: + description: "BackendRefs defines the backend(s) where matching + requests should be sent. \n Failure behavior here depends + on how many BackendRefs are specified and how many are invalid. + \n If *all* entries in BackendRefs are invalid, and there + are also no filters specified in this route rule, *all* traffic + which matches this rule MUST receive a 500 status code. \n + See the HTTPBackendRef definition for the rules about what + makes a single HTTPBackendRef invalid. \n When a HTTPBackendRef + is invalid, 500 status codes MUST be returned for requests + that would have otherwise been routed to an invalid backend. + If multiple backends are specified, and some are invalid, + the proportion of requests that would otherwise have been + routed to an invalid backend MUST receive a 500 status code. + \n For example, if two backends are specified with equal weights, + and one is invalid, 50 percent of traffic must receive a 500. + Implementations may choose how that 50 percent is determined. + \n Support: Core for Kubernetes Service \n Support: Extended + for Kubernetes ServiceImport \n Support: Implementation-specific + for any other resource \n Support for weight: Core" + items: + description: HTTPBackendRef defines how a HTTPRoute should + forward an HTTP request. + properties: + filters: + description: "Filters defined at this level should be + executed if and only if the request is being forwarded + to the backend defined here. \n Support: Implementation-specific + (For broader support of filters, use the Filters field + in HTTPRouteRule.)" + items: + description: HTTPRouteFilter defines processing steps + that must be completed during the request or response + lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway + implementations. Some examples include request or + response modification, implementing authentication + strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type + of the filter. + properties: + extensionRef: + description: "ExtensionRef is an optional, implementation-specific + extension to the \"filter\" behavior. For example, + resource \"myroutefilter\" in group \"networking.example.net\"). + ExtensionRef MUST NOT be used for core and extended + filters. \n Support: Implementation-specific" + properties: + group: + description: Group is the group of the referent. + For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API + group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For + example "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema + for a filter that modifies request headers. \n + Support: Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: "RequestMirror defines a schema for + a filter that mirrors requests. Requests are sent + to the specified destination, but responses from + that destination are ignored. \n Support: Extended" + properties: + backendRef: + description: "BackendRef references a resource + where mirrored requests are sent. \n If the + referent cannot be found, this BackendRef + is invalid and must be dropped from the Gateway. + The controller must ensure the \"ResolvedRefs\" + condition on the Route status is set to `status: + False` and not configure this backend in the + underlying implementation. \n If there is + a cross-namespace reference to an *existing* + object that is not allowed by a ReferenceGrant, + the controller must ensure the \"ResolvedRefs\" + \ condition on the Route is set to `status: + False`, with the \"RefNotPermitted\" reason + and not configure this backend in the underlying + implementation. \n In either error case, the + Message of the `ResolvedRefs` Condition should + be used to provide more detail about the problem. + \n Support: Extended for Kubernetes Service + \n Support: Implementation-specific for any + other resource" + properties: + group: + default: "" + description: Group is the group of the referent. + For example, "gateway.networking.k8s.io". + When unspecified or empty string, core + API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: "Kind is the Kubernetes resource + kind of the referent. For example \"Service\". + \n Defaults to \"Service\" when not specified. + \n ExternalName services can refer to + CNAME DNS records that may live outside + of the cluster and as such are difficult + to reason about in terms of conformance. + They also may not be safe to forward to + (see CVE-2021-25740 for more information). + Implementations SHOULD NOT support ExternalName + Services. \n Support: Core (Services with + a type other than ExternalName) \n Support: + Implementation-specific (Services with + type ExternalName)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace + of the backend. When unspecified, the + local namespace is inferred. \n Note that + when a namespace different than the local + namespace is specified, a ReferenceGrant + object is required in the referent namespace + to allow that namespace's owner to accept + the reference. See the ReferenceGrant + documentation for details. \n Support: + Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: Port specifies the destination + port number to use for this resource. + Port is required when the referent is + a Kubernetes Service. In this case, the + port number is the service port number, + not the target port. For other resources, + destination port might be derived from + the referent resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + required: + - backendRef + type: object + requestRedirect: + description: "RequestRedirect defines a schema for + a filter that responds to the request with an + HTTP redirection. \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be + used in the value of the `Location` header + in the response. When empty, the hostname + in the `Host` header of the request is used. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to + modify the path of the incoming request. The + modified path is then used to construct the + `Location` header. When empty, the request + path is used as-is. \n Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in + the value of the `Location` header in the + response. \n If no port is specified, the + redirect port MUST be derived using the following + rules: \n * If redirect scheme is not-empty, + the redirect port MUST be the well-known port + associated with the redirect scheme. Specifically + \"http\" to port 80 and \"https\" to port + 443. If the redirect scheme does not have + a well-known port, the listener port of the + Gateway SHOULD be used. * If redirect scheme + is empty, the redirect port MUST be the Gateway + Listener port. \n Implementations SHOULD NOT + add the port number in the 'Location' header + in the following cases: \n * A Location header + that will use HTTP (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 80. * A Location header that + will use HTTPS (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 443. \n Support: Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used + in the value of the `Location` header in the + response. When empty, the scheme of the request + is used. \n Scheme redirects can affect the + port of the redirect, for more information, + refer to the documentation for the port field + of this filter. \n Note that values may be + added to this enum, implementations must ensure + that unknown values will not cause a crash. + \n Unknown values here must result in the + implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`. \n Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status + code to be used in response. \n Note that + values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result + in the implementation setting the Accepted + Condition for the Route to `status: False`, + with a Reason of `UnsupportedValue`. \n Support: + Core" + enum: + - 301 + - 302 + type: integer + type: object + responseHeaderModifier: + description: "ResponseHeaderModifier defines a schema + for a filter that modifies response headers. \n + Support: Extended" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: "Type identifies the type of filter + to apply. As with other API fields, types are + classified into three conformance levels: \n - + Core: Filter types and their corresponding configuration + defined by \"Support: Core\" in this package, + e.g. \"RequestHeaderModifier\". All implementations + must support core filters. \n - Extended: Filter + types and their corresponding configuration defined + by \"Support: Extended\" in this package, e.g. + \"RequestMirror\". Implementers are encouraged + to support extended filters. \n - Implementation-specific: + Filters that are defined and supported by specific + vendors. In the future, filters showing convergence + in behavior across multiple implementations will + be considered for inclusion in extended or core + conformance levels. Filter-specific configuration + for such filters is specified using the ExtensionRef + field. `Type` should be set to \"ExtensionRef\" + for custom filters. \n Implementers are encouraged + to define custom implementation types to extend + the core API with implementation-specific behavior. + \n If a reference to a custom filter type cannot + be resolved, the filter MUST NOT be skipped. Instead, + requests that would have been processed by that + filter MUST receive a HTTP error response. \n + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result in + the implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestMirror + - RequestRedirect + - URLRewrite + - ExtensionRef + type: string + urlRewrite: + description: "URLRewrite defines a schema for a + filter that modifies a request during forwarding. + \n Support: Extended" + properties: + hostname: + description: "Hostname is the value to be used + to replace the Host header value during forwarding. + \n Support: Extended" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines a path rewrite. \n + Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + type: object + required: + - type + type: object + maxItems: 16 + type: array + group: + default: "" + description: Group is the group of the referent. For example, + "gateway.networking.k8s.io". When unspecified or empty + string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: "Kind is the Kubernetes resource kind of + the referent. For example \"Service\". \n Defaults to + \"Service\" when not specified. \n ExternalName services + can refer to CNAME DNS records that may live outside + of the cluster and as such are difficult to reason about + in terms of conformance. They also may not be safe to + forward to (see CVE-2021-25740 for more information). + Implementations SHOULD NOT support ExternalName Services. + \n Support: Core (Services with a type other than ExternalName) + \n Support: Implementation-specific (Services with type + ExternalName)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the backend. + When unspecified, the local namespace is inferred. \n + Note that when a namespace different than the local + namespace is specified, a ReferenceGrant object is required + in the referent namespace to allow that namespace's + owner to accept the reference. See the ReferenceGrant + documentation for details. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: Port specifies the destination port number + to use for this resource. Port is required when the + referent is a Kubernetes Service. In this case, the + port number is the service port number, not the target + port. For other resources, destination port might be + derived from the referent resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: "Weight specifies the proportion of requests + forwarded to the referenced backend. This is computed + as weight/(sum of all weights in this BackendRefs list). + For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision + an implementation supports. Weight is not a percentage + and the sum of weights does not need to equal 100. \n + If only one backend is specified and it has a weight + greater than 0, 100% of the traffic is forwarded to + that backend. If weight is set to 0, no traffic should + be forwarded for this entry. If unspecified, weight + defaults to 1. \n Support for this field varies based + on the context where used." + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + maxItems: 16 + type: array + filters: + description: "Filters define the filters that are applied to + requests that match this rule. \n The effects of ordering + of multiple behaviors are currently unspecified. This can + change in the future based on feedback during the alpha stage. + \n Conformance-levels at this level are defined based on the + type of filter: \n - ALL core filters MUST be supported by + all implementations. - Implementers are encouraged to support + extended filters. - Implementation-specific custom filters + have no API guarantees across implementations. \n Specifying + a core filter multiple times has unspecified or implementation-specific + conformance. \n All filters are expected to be compatible + with each other except for the URLRewrite and RequestRedirect + filters, which may not be combined. If an implementation can + not support other combinations of filters, they must clearly + document that limitation. In all cases where incompatible + or unsupported filters are specified, implementations MUST + add a warning condition to status. \n Support: Core" + items: + description: HTTPRouteFilter defines processing steps that + must be completed during the request or response lifecycle. + HTTPRouteFilters are meant as an extension point to express + processing that may be done in Gateway implementations. + Some examples include request or response modification, + implementing authentication strategies, rate-limiting, and + traffic shaping. API guarantee/conformance is defined based + on the type of the filter. + properties: + extensionRef: + description: "ExtensionRef is an optional, implementation-specific + extension to the \"filter\" behavior. For example, + resource \"myroutefilter\" in group \"networking.example.net\"). + ExtensionRef MUST NOT be used for core and extended + filters. \n Support: Implementation-specific" + properties: + group: + description: Group is the group of the referent. For + example, "gateway.networking.k8s.io". When unspecified + or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For example + "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema for + a filter that modifies request headers. \n Support: + Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It appends + to any existing values associated with the header + name. \n Input: GET /foo HTTP/1.1 my-header: foo + \n Config: add: - name: \"my-header\" value: \"bar,baz\" + \n Output: GET /foo HTTP/1.1 my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from the + HTTP request before the action. The value of Remove + is a list of HTTP header names. Note that the header + names are case-insensitive (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo my-header2: + bar my-header3: baz \n Config: remove: [\"my-header1\", + \"my-header3\"] \n Output: GET /foo HTTP/1.1 my-header2: + bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with the + given header (name, value) before the action. \n + Input: GET /foo HTTP/1.1 my-header: foo \n Config: + set: - name: \"my-header\" value: \"bar\" \n Output: + GET /foo HTTP/1.1 my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: "RequestMirror defines a schema for a filter + that mirrors requests. Requests are sent to the specified + destination, but responses from that destination are + ignored. \n Support: Extended" + properties: + backendRef: + description: "BackendRef references a resource where + mirrored requests are sent. \n If the referent cannot + be found, this BackendRef is invalid and must be + dropped from the Gateway. The controller must ensure + the \"ResolvedRefs\" condition on the Route status + is set to `status: False` and not configure this + backend in the underlying implementation. \n If + there is a cross-namespace reference to an *existing* + object that is not allowed by a ReferenceGrant, + the controller must ensure the \"ResolvedRefs\" + \ condition on the Route is set to `status: False`, + with the \"RefNotPermitted\" reason and not configure + this backend in the underlying implementation. \n + In either error case, the Message of the `ResolvedRefs` + Condition should be used to provide more detail + about the problem. \n Support: Extended for Kubernetes + Service \n Support: Implementation-specific for + any other resource" + properties: + group: + default: "" + description: Group is the group of the referent. + For example, "gateway.networking.k8s.io". When + unspecified or empty string, core API group + is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: "Kind is the Kubernetes resource + kind of the referent. For example \"Service\". + \n Defaults to \"Service\" when not specified. + \n ExternalName services can refer to CNAME + DNS records that may live outside of the cluster + and as such are difficult to reason about in + terms of conformance. They also may not be safe + to forward to (see CVE-2021-25740 for more information). + Implementations SHOULD NOT support ExternalName + Services. \n Support: Core (Services with a + type other than ExternalName) \n Support: Implementation-specific + (Services with type ExternalName)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the + backend. When unspecified, the local namespace + is inferred. \n Note that when a namespace different + than the local namespace is specified, a ReferenceGrant + object is required in the referent namespace + to allow that namespace's owner to accept the + reference. See the ReferenceGrant documentation + for details. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: Port specifies the destination port + number to use for this resource. Port is required + when the referent is a Kubernetes Service. In + this case, the port number is the service port + number, not the target port. For other resources, + destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + required: + - backendRef + type: object + requestRedirect: + description: "RequestRedirect defines a schema for a filter + that responds to the request with an HTTP redirection. + \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be used + in the value of the `Location` header in the response. + When empty, the hostname in the `Host` header of + the request is used. \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to modify + the path of the incoming request. The modified path + is then used to construct the `Location` header. + When empty, the request path is used as-is. \n Support: + Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the value + with which to replace the full path of a request + during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies the + value with which to replace the prefix match + of a request during a rewrite or redirect. For + example, a request to \"/foo/bar\" with a prefix + match of \"/foo\" and a ReplacePrefixMatch of + \"/xyz\" would be modified to \"/xyz/bar\". + \n Note that this matches the behavior of the + PathPrefix match type. This matches full path + elements. A path element refers to the list + of labels in the path split by the `/` separator. + When specified, a trailing `/` is ignored. For + example, the paths `/abc`, `/abc/`, and `/abc/def` + would all match the prefix `/abc`, but the path + `/abcd` would not. \n Request Path | Prefix + Match | Replace Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | /xyz/ + \ | /xyz/bar /foo/bar | /foo/ | + /xyz | /xyz/bar /foo/bar | /foo/ + \ | /xyz/ | /xyz/bar /foo | + /foo | /xyz | /xyz /foo/ | + /foo | /xyz | /xyz/ /foo/bar + \ | /foo | | /bar + /foo/ | /foo | + | / /foo | /foo | + | / /foo/ | /foo | / | + / /foo | /foo | / | + /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path modifier. + Additional types may be added in a future release + of the API. \n Note that values may be added + to this enum, implementations must ensure that + unknown values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the Route + to `status: False`, with a Reason of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in the value + of the `Location` header in the response. \n If + no port is specified, the redirect port MUST be + derived using the following rules: \n * If redirect + scheme is not-empty, the redirect port MUST be the + well-known port associated with the redirect scheme. + Specifically \"http\" to port 80 and \"https\" to + port 443. If the redirect scheme does not have a + well-known port, the listener port of the Gateway + SHOULD be used. * If redirect scheme is empty, the + redirect port MUST be the Gateway Listener port. + \n Implementations SHOULD NOT add the port number + in the 'Location' header in the following cases: + \n * A Location header that will use HTTP (whether + that is determined via the Listener protocol or + the Scheme field) _and_ use port 80. * A Location + header that will use HTTPS (whether that is determined + via the Listener protocol or the Scheme field) _and_ + use port 443. \n Support: Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used in the + value of the `Location` header in the response. + When empty, the scheme of the request is used. \n + Scheme redirects can affect the port of the redirect, + for more information, refer to the documentation + for the port field of this filter. \n Note that + values may be added to this enum, implementations + must ensure that unknown values will not cause a + crash. \n Unknown values here must result in the + implementation setting the Accepted Condition for + the Route to `status: False`, with a Reason of `UnsupportedValue`. + \n Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status code to + be used in response. \n Note that values may be + added to this enum, implementations must ensure + that unknown values will not cause a crash. \n Unknown + values here must result in the implementation setting + the Accepted Condition for the Route to `status: + False`, with a Reason of `UnsupportedValue`. \n + Support: Core" + enum: + - 301 + - 302 + type: integer + type: object + responseHeaderModifier: + description: "ResponseHeaderModifier defines a schema + for a filter that modifies response headers. \n Support: + Extended" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It appends + to any existing values associated with the header + name. \n Input: GET /foo HTTP/1.1 my-header: foo + \n Config: add: - name: \"my-header\" value: \"bar,baz\" + \n Output: GET /foo HTTP/1.1 my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from the + HTTP request before the action. The value of Remove + is a list of HTTP header names. Note that the header + names are case-insensitive (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo my-header2: + bar my-header3: baz \n Config: remove: [\"my-header1\", + \"my-header3\"] \n Output: GET /foo HTTP/1.1 my-header2: + bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with the + given header (name, value) before the action. \n + Input: GET /foo HTTP/1.1 my-header: foo \n Config: + set: - name: \"my-header\" value: \"bar\" \n Output: + GET /foo HTTP/1.1 my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: "Type identifies the type of filter to apply. + As with other API fields, types are classified into + three conformance levels: \n - Core: Filter types and + their corresponding configuration defined by \"Support: + Core\" in this package, e.g. \"RequestHeaderModifier\". + All implementations must support core filters. \n - + Extended: Filter types and their corresponding configuration + defined by \"Support: Extended\" in this package, e.g. + \"RequestMirror\". Implementers are encouraged to support + extended filters. \n - Implementation-specific: Filters + that are defined and supported by specific vendors. + In the future, filters showing convergence in behavior + across multiple implementations will be considered for + inclusion in extended or core conformance levels. Filter-specific + configuration for such filters is specified using the + ExtensionRef field. `Type` should be set to \"ExtensionRef\" + for custom filters. \n Implementers are encouraged to + define custom implementation types to extend the core + API with implementation-specific behavior. \n If a reference + to a custom filter type cannot be resolved, the filter + MUST NOT be skipped. Instead, requests that would have + been processed by that filter MUST receive a HTTP error + response. \n Note that values may be added to this enum, + implementations must ensure that unknown values will + not cause a crash. \n Unknown values here must result + in the implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason of `UnsupportedValue`." + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestMirror + - RequestRedirect + - URLRewrite + - ExtensionRef + type: string + urlRewrite: + description: "URLRewrite defines a schema for a filter + that modifies a request during forwarding. \n Support: + Extended" + properties: + hostname: + description: "Hostname is the value to be used to + replace the Host header value during forwarding. + \n Support: Extended" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines a path rewrite. \n Support: + Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the value + with which to replace the full path of a request + during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies the + value with which to replace the prefix match + of a request during a rewrite or redirect. For + example, a request to \"/foo/bar\" with a prefix + match of \"/foo\" and a ReplacePrefixMatch of + \"/xyz\" would be modified to \"/xyz/bar\". + \n Note that this matches the behavior of the + PathPrefix match type. This matches full path + elements. A path element refers to the list + of labels in the path split by the `/` separator. + When specified, a trailing `/` is ignored. For + example, the paths `/abc`, `/abc/`, and `/abc/def` + would all match the prefix `/abc`, but the path + `/abcd` would not. \n Request Path | Prefix + Match | Replace Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | /xyz/ + \ | /xyz/bar /foo/bar | /foo/ | + /xyz | /xyz/bar /foo/bar | /foo/ + \ | /xyz/ | /xyz/bar /foo | + /foo | /xyz | /xyz /foo/ | + /foo | /xyz | /xyz/ /foo/bar + \ | /foo | | /bar + /foo/ | /foo | + | / /foo | /foo | + | / /foo/ | /foo | / | + / /foo | /foo | / | + /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path modifier. + Additional types may be added in a future release + of the API. \n Note that values may be added + to this enum, implementations must ensure that + unknown values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the Route + to `status: False`, with a Reason of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + type: object + required: + - type + type: object + maxItems: 16 + type: array + matches: + default: + - path: + type: PathPrefix + value: / + description: "Matches define conditions used for matching the + rule against incoming HTTP requests. Each match is independent, + i.e. this rule will be matched if **any** one of the matches + is satisfied. \n For example, take the following matches configuration: + \n ``` matches: - path: value: \"/foo\" headers: - name: \"version\" + value: \"v2\" - path: value: \"/v2/foo\" ``` \n For a request + to match against this rule, a request must satisfy EITHER + of the two conditions: \n - path prefixed with `/foo` AND + contains the header `version: v2` - path prefix of `/v2/foo` + \n See the documentation for HTTPRouteMatch on how to specify + multiple match conditions that should be ANDed together. \n + If no matches are specified, the default is a prefix path + match on \"/\", which has the effect of matching every HTTP + request. \n Proxy or Load Balancer routing configuration generated + from HTTPRoutes MUST prioritize matches based on the following + criteria, continuing on ties. Across all rules specified on + applicable Routes, precedence must be given to the match having: + \n * \"Exact\" path match. * \"Prefix\" path match with largest + number of characters. * Method match. * Largest number of + header matches. * Largest number of query param matches. \n + Note: The precedence of RegularExpression path matches are + implementation-specific. \n If ties still exist across multiple + Routes, matching precedence MUST be determined in order of + the following criteria, continuing on ties: \n * The oldest + Route based on creation timestamp. * The Route appearing first + in alphabetical order by \"{namespace}/{name}\". \n If ties + still exist within an HTTPRoute, matching precedence MUST + be granted to the FIRST matching rule (in list order) with + a match meeting the above criteria. \n When no rules matching + a request have been successfully attached to the parent a + request is coming from, a HTTP 404 status code MUST be returned." + items: + description: "HTTPRouteMatch defines the predicate used to + match requests to a given action. Multiple match types are + ANDed together, i.e. the match will evaluate to true only + if all conditions are satisfied. \n For example, the match + below will match a HTTP request only if its path starts + with `/foo` AND it contains the `version: v1` header: \n + ``` match: \n path: value: \"/foo\" headers: - name: \"version\" + value \"v1\" \n ```" + properties: + headers: + description: Headers specifies HTTP request header matchers. + Multiple match values are ANDed together, meaning, a + request must match all the specified headers to select + the route. + items: + description: HTTPHeaderMatch describes how to select + a HTTP route by matching HTTP request headers. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case insensitive. + (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent header + names, only the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST be + ignored. Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered equivalent. + \n When a header is repeated in an HTTP request, + it is implementation-specific behavior as to how + this is represented. Generally, proxies should + follow the guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, with special + handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the header. \n Support: Core (Exact) + \n Support: Implementation-specific (RegularExpression) + \n Since RegularExpression HeaderMatchType has + implementation-specific conformance, implementations + can support POSIX, PCRE or any other dialects + of regular expressions. Please read the implementation's + documentation to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP Header to + be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. When + specified, this route will be matched only if the request + has the specified method. \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path matcher. + If this field is not specified, a default prefix match + on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match against + the path Value. \n Support: Core (Exact, PathPrefix) + \n Support: Implementation-specific (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match against. + maxLength: 1024 + type: string + type: object + queryParams: + description: "QueryParams specifies HTTP query parameter + matchers. Multiple match values are ANDed together, + meaning, a request must match all the specified query + parameters to select the route. \n Support: Extended" + items: + description: HTTPQueryParamMatch describes how to select + a HTTP route by matching HTTP query parameters. + properties: + name: + description: "Name is the name of the HTTP query + param to be matched. This must be an exact string + match. (See https://tools.ietf.org/html/rfc7230#section-2.7.3). + \n If multiple entries specify equivalent query + param names, only the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent query param name MUST + be ignored. \n If a query param is repeated in + an HTTP request, the behavior is purposely left + undefined, since different data planes have different + capabilities. However, it is *recommended* that + implementations should match against the first + value of the param if the data plane supports + it, as this behavior is expected in other load + balancing contexts outside of the Gateway API. + \n Users SHOULD NOT route traffic based on repeated + query params to guard themselves against potential + differences in the implementations." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the query parameter. \n Support: + Extended (Exact) \n Support: Implementation-specific + (RegularExpression) \n Since RegularExpression + QueryParamMatchType has Implementation-specific + conformance, implementations can support POSIX, + PCRE or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP query param + to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + maxItems: 8 + type: array + type: object + maxItems: 16 + type: array + type: object + status: + description: Status defines the current state of HTTPRoute. + properties: + parents: + description: "Parents is a list of parent resources (usually Gateways) + that are associated with the route, and the status of the route + with respect to each parent. When this route attaches to a parent, + the controller that manages the parent must add an entry to this + list when the controller first sees the route and should update + the entry as appropriate when the route or gateway is modified. + \n Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this + API can only populate Route status for the Gateways/parent resources + they are responsible for. \n A maximum of 32 Gateways will be represented + in this list. An empty list means the route has not been attached + to any Gateway." + items: + description: RouteParentStatus describes the status of a route with + respect to an associated Parent. + properties: + conditions: + description: "Conditions describes the status of the route with + respect to the Gateway. Note that the route's availability + is also subject to the Gateway's own status conditions and + listener status. \n If the Route's ParentRef specifies an + existing Gateway that supports Routes of this kind AND that + Gateway's controller has sufficient access, then that Gateway's + controller MUST set the \"Accepted\" condition on the Route, + to indicate whether the route has been accepted or rejected + by the Gateway, and why. \n A Route MUST be considered \"Accepted\" + if at least one of the Route's rules is implemented by the + Gateway. \n There are a number of cases where the \"Accepted\" + condition may not be set due to lack of controller visibility, + that includes when: \n * The Route refers to a non-existent + parent. * The Route is of a type that the controller does + not support. * The Route is in a namespace the controller + does not have access to." + items: + description: "Condition contains details for one aspect of + the current state of this API Resource. --- This struct + is intended for direct use as an array at the field path + .status.conditions. For example, \n type FooStatus struct{ + // Represents the observations of a foo's current state. + // Known .status.conditions.type are: \"Available\", \"Progressing\", + and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields + }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should + be when the underlying condition changed. If that is + not known, then using the time when the API field changed + is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, + if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the + current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier + indicating the reason for the condition's last transition. + Producers of specific condition types may define expected + values and meanings for this field, and whether the + values are considered a guaranteed API. The value should + be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across + resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability + to deconflict is important. The regex it matches is + (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: "ControllerName is a domain/path string that indicates + the name of the controller that wrote this status. This corresponds + with the controllerName field on GatewayClass. \n Example: + \"example.net/gateway-controller\". \n The format of this + field is DOMAIN \"/\" PATH, where DOMAIN and PATH are valid + Kubernetes names (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + \n Controllers MUST populate this field when writing status. + Controllers should ensure that entries to status populated + with their ControllerName are cleaned up when they are no + longer necessary." + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: ParentRef corresponds with a ParentRef in the spec + that this RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: "Group is the group of the referent. When unspecified, + \"gateway.networking.k8s.io\" is inferred. To set the + core API group (such as for a \"Service\" kind referent), + Group must be explicitly set to \"\" (empty string). \n + Support: Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: + Core (Gateway) \n Support: Implementation-specific (Other + Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. + When unspecified, this refers to the local namespace of + the Route. \n Note that there are specific rules for ParentRefs + which cross namespace boundaries. Cross-namespace references + are only valid if they are explicitly allowed by something + in the namespace they are referring to. For example: Gateway + has the AllowedRoutes field, and ReferenceGrant provides + a generic way to enable any other kind of cross-namespace + reference. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: "Port is the network port this Route targets. + It can be interpreted differently based on the type of + parent resource. \n When the parent resource is a Gateway, + this targets all listeners listening on the specified + port that also support this kind of Route(and select this + Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to + a specific port as opposed to a listener(s) whose port(s) + may be changed. When both Port and SectionName are specified, + the name and port of the selected listener must match + both specified values. \n Implementations MAY choose to + support other parent resources. Implementations supporting + other types of parent resources MUST clearly document + how/if Port is interpreted. \n For the purpose of status, + an attachment is considered successful as long as the + parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them + by Route kind, namespace, or hostname. If 1 of 2 Gateway + listeners accept attachment from the referencing Route, + the Route MUST be considered successfully attached. If + no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + \n Support: Extended \n " + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: "SectionName is the name of a section within + the target resource. In the following resources, SectionName + is interpreted as the following: \n * Gateway: Listener + Name. When both Port (experimental) and SectionName are + specified, the name and port of the selected listener + must match both specified values. \n Implementations MAY + choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName + is interpreted. \n When unspecified (empty string), this + will reference the entire resource. For the purpose of + status, an attachment is considered successful if at least + one section in the parent resource accepts it. For example, + Gateway listeners can restrict which Routes can attach + to them by Route kind, namespace, or hostname. If 1 of + 2 Gateway listeners accept attachment from the referencing + Route, the Route MUST be considered successfully attached. + If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null +{{- end }} diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/authorization-policy.yaml b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/authorization-policy.yaml new file mode 100644 index 0000000..7d86520 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/authorization-policy.yaml @@ -0,0 +1,99 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: authorizationpolicies.policy.linkerd.io + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + linkerd.io/control-plane-ns: {{.Release.Namespace}} +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: AuthorizationPolicy + plural: authorizationpolicies + singular: authorizationpolicy + shortNames: [authzpolicy] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied server + resources. + type: object + required: [targetRef, requiredAuthenticationRefs] + properties: + targetRef: + description: >- + TargetRef references a resource to which the authorization + policy applies. + type: object + required: [kind, name] + # Modified from the gateway API. + # Copyright 2020 The Kubernetes Authors + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + requiredAuthenticationRefs: + description: >- + RequiredAuthenticationRefs enumerates a set of required + authentications. ALL authentications must be satisfied for + the authorization to apply. If any of the referred objects + cannot be found, the authorization will be ignored. + type: array + items: + type: object + required: [kind, name] + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/httproute.yaml b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/httproute.yaml new file mode 100644 index 0000000..9aaaefc --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/httproute.yaml @@ -0,0 +1,5216 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: httproutes.policy.linkerd.io + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + linkerd.io/control-plane-ns: {{.Release.Namespace}} +spec: + group: policy.linkerd.io + names: + kind: HTTPRoute + listKind: HTTPRouteList + plural: httproutes + singular: httproute + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.hostnames + name: Hostnames + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: HTTPRoute provides a way to route HTTP requests. This includes + the capability to match requests by hostname, path, header, or query param. + Filters can be used to specify additional processing steps. Backends specify + where matching requests should be routed. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of HTTPRoute. + properties: + hostnames: + description: "Hostnames defines a set of hostname that should match + against the HTTP Host header to select a HTTPRoute to process the + request. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname may + be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n If a hostname is specified + by both the Listener and HTTPRoute, there must be at least one intersecting + hostname for the HTTPRoute to be attached to the Listener. For example: + \n * A Listener with `test.example.com` as the hostname matches + HTTPRoutes that have either not specified any hostnames, or have + specified at least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches HTTPRoutes + \ that have either not specified any hostnames or have specified + at least one hostname that matches the Listener hostname. For + example, `*.example.com`, `test.example.com`, and `foo.test.example.com` + would all match. On the other hand, `example.com` and `test.example.net` + would not match. \n Hostnames that are prefixed with a wildcard + label (`*.`) are interpreted as a suffix match. That means that + a match for `*.example.com` would match both `test.example.com`, + and `foo.test.example.com`, but not `example.com`. \n If both the + Listener and HTTPRoute have specified hostnames, any HTTPRoute hostnames + that do not match the Listener hostname MUST be ignored. For example, + if a Listener specified `*.example.com`, and the HTTPRoute specified + `test.example.com` and `test.example.net`, `test.example.net` must + not be considered for a match. \n If both the Listener and HTTPRoute + have specified hostnames, and none match with the criteria above, + then the HTTPRoute is not accepted. The implementation must raise + an 'Accepted' Condition with a status of `False` in the corresponding + RouteParentStatus. \n Support: Core" + items: + description: "Hostname is the fully qualified domain name of a network + host. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname + may be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n Hostname can be \"precise\" + which is a domain name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", which is a domain + name prefixed with a single wildcard label (e.g. `*.example.com`). + \n Note that as per RFC1035 and RFC1123, a *label* must consist + of lower case alphanumeric characters or '-', and must start and + end with an alphanumeric character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + parentRefs: + description: "ParentRefs references the resources (usually Gateways) + that a Route wants to be attached to. Note that the referenced parent + resource needs to allow this for the attachment to be complete. + For Gateways, that means the Gateway needs to allow attachment from + Routes of this kind and namespace. \n The only kind of parent resource + with \"Core\" support is Gateway. This API may be extended in the + future to support additional kinds of parent resources such as one + of the route kinds. \n It is invalid to reference an identical parent + more than once. It is valid to reference multiple distinct sections + within the same parent resource, such as 2 Listeners within a Gateway. + \n It is possible to separately reference multiple distinct objects + that may be collapsed by an implementation. For example, some implementations + may choose to merge compatible Gateway Listeners together. If that + is the case, the list of routes attached to those resources should + also be merged." + items: + description: "ParentReference identifies an API object (usually + a Gateway) that can be considered a parent of this resource (usually + a route). The only kind of parent resource with \"Core\" support + is Gateway. This API may be extended in the future to support + additional kinds of parent resources, such as HTTPRoute. \n The + API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid." + properties: + group: + default: policy.linkerd.io + description: "Group is the group of the referent. \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: Core + (Gateway) Support: Custom (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. When + unspecified (or empty string), this refers to the local namespace + of the Route. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: "port" + type: integer + sectionName: + description: "SectionName is the name of a section within the + target resource. In the following resources, SectionName is + interpreted as the following: \n * Gateway: Listener Name. + When both Port (experimental) and SectionName are specified, + the name and port of the selected listener must match both + specified values. \n Implementations MAY choose to support + attaching Routes to other resources. If that is the case, + they MUST clearly document how SectionName is interpreted. + \n When unspecified (empty string), this will reference the + entire resource. For the purpose of status, an attachment + is considered successful if at least one section in the parent + resource accepts it. For example, Gateway listeners can restrict + which Routes can attach to them by Route kind, namespace, + or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this + Route, the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + rules: + default: + - matches: + - path: + type: PathPrefix + value: / + description: Rules are a list of HTTP matchers, filters and actions. + items: + description: HTTPRouteRule defines semantics for matching an HTTP + request based on conditions (matches) and processing it (filters). + properties: + backendRefs: + type: array + items: + type: object + properties: + name: + type: string + port: + type: integer + namespace: + type: string + default: "default" + filters: + description: "Filters defined at this level should be + executed if and only if the request is being forwarded + to the backend defined here. \n Support: Implementation-specific + (For broader support of filters, use the Filters field + in HTTPRouteRule.)" + items: + description: HTTPRouteFilter defines processing steps + that must be completed during the request or response + lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway + implementations. Some examples include request or + response modification, implementing authentication + strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type + of the filter. + properties: + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema + for a filter that modifies request headers. \n + Support: Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestRedirect: + description: "RequestRedirect defines a schema for + a filter that responds to the request with an + HTTP redirection. \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be + used in the value of the `Location` header + in the response. When empty, the hostname + in the `Host` header of the request is used. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to + modify the path of the incoming request. The + modified path is then used to construct the + `Location` header. When empty, the request + path is used as-is. \n Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in + the value of the `Location` header in the + response. \n If no port is specified, the + redirect port MUST be derived using the following + rules: \n * If redirect scheme is not-empty, + the redirect port MUST be the well-known port + associated with the redirect scheme. Specifically + \"http\" to port 80 and \"https\" to port + 443. If the redirect scheme does not have + a well-known port, the listener port of the + Gateway SHOULD be used. * If redirect scheme + is empty, the redirect port MUST be the Gateway + Listener port. \n Implementations SHOULD NOT + add the port number in the 'Location' header + in the following cases: \n * A Location header + that will use HTTP (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 80. * A Location header that + will use HTTPS (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 443. \n Support: Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used + in the value of the `Location` header in the + response. When empty, the scheme of the request + is used. \n Scheme redirects can affect the + port of the redirect, for more information, + refer to the documentation for the port field + of this filter. \n Note that values may be + added to this enum, implementations must ensure + that unknown values will not cause a crash. + \n Unknown values here must result in the + implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`. \n Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status + code to be used in response. \n Note that + values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result + in the implementation setting the Accepted + Condition for the Route to `status: False`, + with a Reason of `UnsupportedValue`. \n Support: + Core" + enum: + - 301 + - 302 + type: integer + type: object + responseHeaderModifier: + description: "ResponseHeaderModifier defines a schema + for a filter that modifies response headers. \n + Support: Extended" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: "Type identifies the type of filter + to apply. As with other API fields, types are + classified into three conformance levels: \n - + Core: Filter types and their corresponding configuration + defined by \"Support: Core\" in this package, + e.g. \"RequestHeaderModifier\". All implementations + must support core filters. \n - Extended: Filter + types and their corresponding configuration defined + by \"Support: Extended\" in this package, e.g. + \"RequestMirror\". Implementers are encouraged + to support extended filters. \n - Implementation-specific: + Filters that are defined and supported by specific + vendors. In the future, filters showing convergence + in behavior across multiple implementations will + be considered for inclusion in extended or core + conformance levels. Filter-specific configuration + for such filters is specified using the ExtensionRef + field. `Type` should be set to \"ExtensionRef\" + for custom filters. \n Implementers are encouraged + to define custom implementation types to extend + the core API with implementation-specific behavior. + \n If a reference to a custom filter type cannot + be resolved, the filter MUST NOT be skipped. Instead, + requests that would have been processed by that + filter MUST receive a HTTP error response. \n + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result in + the implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestRedirect + type: string + required: + - type + type: object + maxItems: 16 + type: array + filters: + description: "Filters define the filters that are applied to + requests that match this rule. \n The effects of ordering + of multiple behaviors are currently unspecified. This can + change in the future based on feedback during the alpha stage. + \n Conformance-levels at this level are defined based on the + type of filter: \n - ALL core filters MUST be supported by + all implementations. - Implementers are encouraged to support + extended filters. - Implementation-specific custom filters + have no API guarantees across implementations. \n Specifying + a core filter multiple times has unspecified or custom conformance. + \n All filters are expected to be compatible with each other + except for the URLRewrite and RequestRedirect filters, which + may not be combined. If an implementation can not support + other combinations of filters, they must clearly document + that limitation. In all cases where incompatible or unsupported + filters are specified, implementations MUST add a warning + condition to status. \n Support: Core" + items: + description: HTTPRouteFilter defines processing steps that + must be completed during the request or response lifecycle. + HTTPRouteFilters are meant as an extension point to express + processing that may be done in Gateway implementations. + Some examples include request or response modification, + implementing authentication strategies, rate-limiting, and + traffic shaping. API guarantee/conformance is defined based + on the type of the filter. + properties: + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema for + a filter that modifies request headers. \n Support: + Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It appends + to any existing values associated with the header + name. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: add: - name: \"my-header\" value: + \"bar\" \n Output: GET /foo HTTP/1.1 my-header: + foo my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from the + HTTP request before the action. The value of Remove + is a list of HTTP header names. Note that the header + names are case-insensitive (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + \ my-header2: bar my-header3: baz \n Config: + \ remove: [\"my-header1\", \"my-header3\"] \n Output: + \ GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with the + given header (name, value) before the action. \n + Input: GET /foo HTTP/1.1 my-header: foo \n Config: + \ set: - name: \"my-header\" value: \"bar\" + \n Output: GET /foo HTTP/1.1 my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestRedirect: + description: "RequestRedirect defines a schema for a filter + that responds to the request with an HTTP redirection. + \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be used + in the value of the `Location` header in the response. + When empty, the hostname of the request is used. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to + modify the path of the incoming request. The + modified path is then used to construct the + `Location` header. When empty, the request + path is used as-is. \n Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in the value + of the `Location` header in the response. When empty, + port (if specified) of the request is used. \n Support: + Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used in the + value of the `Location` header in the response. + When empty, the scheme of the request is used. \n + Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status code to + be used in response. \n Support: Core" + enum: + - 301 + - 302 + type: integer + type: object + type: + description: "Type identifies the type of filter to apply. + As with other API fields, types are classified into + three conformance levels: \n - Core: Filter types and + their corresponding configuration defined by \"Support: + Core\" in this package, e.g. \"RequestHeaderModifier\". + All implementations must support core filters. \n\n " + enum: + - RequestHeaderModifier + - RequestRedirect + type: string + required: + - type + type: object + maxItems: 16 + type: array + matches: + default: + - path: + type: PathPrefix + value: / + description: "Matches define conditions used for matching the + rule against incoming HTTP requests. Each match is independent, + i.e. this rule will be matched if **any** one of the matches + is satisfied. \n For example, take the following matches configuration: + \n ``` matches: - path: value: \"/foo\" headers: - + name: \"version\" value: \"v2\" - path: value: \"/v2/foo\" + ``` \n For a request to match against this rule, a request + must satisfy EITHER of the two conditions: \n - path prefixed + with `/foo` AND contains the header `version: v2` - path prefix + of `/v2/foo` \n See the documentation for HTTPRouteMatch on + how to specify multiple match conditions that should be ANDed + together. \n If no matches are specified, the default is a + prefix path match on \"/\", which has the effect of matching + every HTTP request. \n Proxy or Load Balancer routing configuration + generated from HTTPRoutes MUST prioritize rules based on the + following criteria, continuing on ties. Precedence must be + given to the the Rule with the largest number of: \n * Characters + in a matching non-wildcard hostname. * Characters in a matching + hostname. * Characters in a matching path. * Header matches. + * Query param matches. \n If ties still exist across multiple + Routes, matching precedence MUST be determined in order of + the following criteria, continuing on ties: \n * The oldest + Route based on creation timestamp. * The Route appearing first + in alphabetical order by \"{namespace}/{name}\". \n If ties + still exist within the Route that has been given precedence, + matching precedence MUST be granted to the first matching + rule meeting the above criteria. \n When no rules matching + a request have been successfully attached to the parent a + request is coming from, a HTTP 404 status code MUST be returned." + items: + description: "HTTPRouteMatch defines the predicate used to + match requests to a given action. Multiple match types are + ANDed together, i.e. the match will evaluate to true only + if all conditions are satisfied. \n For example, the match + below will match a HTTP request only if its path starts + with `/foo` AND it contains the `version: v1` header: \n + ``` match: path: value: \"/foo\" headers: - name: + \"version\" value \"v1\" ```" + properties: + headers: + description: Headers specifies HTTP request header matchers. + Multiple match values are ANDed together, meaning, a + request must match all the specified headers to select + the route. + items: + description: HTTPHeaderMatch describes how to select + a HTTP route by matching HTTP request headers. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case insensitive. + (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent header + names, only the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST be + ignored. Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered equivalent. + \n When a header is repeated in an HTTP request, + it is implementation-specific behavior as to how + this is represented. Generally, proxies should + follow the guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, with special + handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the header. \n Support: Core (Exact) + \n Support: Custom (RegularExpression) \n Since + RegularExpression HeaderMatchType has custom conformance, + implementations can support POSIX, PCRE or any + other dialects of regular expressions. Please + read the implementation's documentation to determine + the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP Header to + be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. When + specified, this route will be matched only if the request + has the specified method. \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path matcher. + If this field is not specified, a default prefix match + on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match against + the path Value. \n Support: Core (Exact, PathPrefix) + \n Support: Custom (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match against. + maxLength: 1024 + type: string + type: object + queryParams: + description: QueryParams specifies HTTP query parameter + matchers. Multiple match values are ANDed together, + meaning, a request must match all the specified query + parameters to select the route. + items: + description: HTTPQueryParamMatch describes how to select + a HTTP route by matching HTTP query parameters. + properties: + name: + description: Name is the name of the HTTP query + param to be matched. This must be an exact string + match. (See https://tools.ietf.org/html/rfc7230#section-2.7.3). + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the query parameter. \n Support: + Extended (Exact) \n Support: Custom (RegularExpression) + \n Since RegularExpression QueryParamMatchType + has custom conformance, implementations can support + POSIX, PCRE or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP query param + to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + maxItems: 8 + type: array + type: object + maxItems: 16 + type: array + type: object + status: + description: Status defines the current state of HTTPRoute. + properties: + parents: + description: "Parents is a list of parent resources (usually Gateways) + that are associated with the route, and the status of the route + with respect to each parent. When this route attaches to a parent, + the controller that manages the parent must add an entry to this + list when the controller first sees the route and should update + the entry as appropriate when the route or gateway is modified. + \n Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this + API can only populate Route status for the Gateways/parent resources + they are responsible for. \n A maximum of 32 Gateways will be represented + in this list. An empty list means the route has not been attached + to any Gateway." + items: + description: RouteParentStatus describes the status of a route with + respect to an associated Parent. + properties: + conditions: + description: "Conditions describes the status of the route with + respect to the Gateway. Note that the route's availability + is also subject to the Gateway's own status conditions and + listener status. \n If the Route's ParentRef specifies an + existing Gateway that supports Routes of this kind AND that + Gateway's controller has sufficient access, then that Gateway's + controller MUST set the \"Accepted\" condition on the Route, + to indicate whether the route has been accepted or rejected + by the Gateway, and why. \n A Route MUST be considered \"Accepted\" + if at least one of the Route's rules is implemented by the + Gateway. \n There are a number of cases where the \"Accepted\" + condition may not be set due to lack of controller visibility, + that includes when: \n * The Route refers to a non-existent + parent. * The Route is of a type that the controller does + not support. * The Route is in a namespace the the controller + does not have access to." + items: + description: "Condition contains details for one aspect of + the current state of this API Resource. --- This struct + is intended for direct use as an array at the field path + .status.conditions. For example, type FooStatus struct{ + \ // Represents the observations of a foo's current state. + \ // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type + \ // +patchStrategy=merge // +listType=map // + +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` + \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should + be when the underlying condition changed. If that is + not known, then using the time when the API field changed + is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, + if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the + current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier + indicating the reason for the condition's last transition. + Producers of specific condition types may define expected + values and meanings for this field, and whether the + values are considered a guaranteed API. The value should + be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across + resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability + to deconflict is important. The regex it matches is + (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: "ControllerName is a domain/path string that indicates + the name of the controller that wrote this status. This corresponds + with the controllerName field on GatewayClass. \n Example: + \"example.net/gateway-controller\". \n The format of this + field is DOMAIN \"/\" PATH, where DOMAIN and PATH are valid + Kubernetes names (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + \n Controllers MUST populate this field when writing status. + Controllers should ensure that entries to status populated + with their ControllerName are cleaned up when they are no + longer necessary." + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: ParentRef corresponds with a ParentRef in the spec + that this RouteParentStatus struct describes the status of. + properties: + group: + default: policy.linkerd.io + description: "Group is the group of the referent. \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: + Core (Gateway) Support: Custom (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. + When unspecified (or empty string), this refers to the + local namespace of the Route. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + sectionName: + description: "SectionName is the name of a section within + the target resource. In the following resources, SectionName + is interpreted as the following: \n * Gateway: Listener + Name. When both Port (experimental) and SectionName are + specified, the name and port of the selected listener + must match both specified values. \n Implementations MAY + choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName + is interpreted. \n When unspecified (empty string), this + will reference the entire resource. For the purpose of + status, an attachment is considered successful if at least + one section in the parent resource accepts it. For example, + Gateway listeners can restrict which Routes can attach + to them by Route kind, namespace, or hostname. If 1 of + 2 Gateway listeners accept attachment from the referencing + Route, the Route MUST be considered successfully attached. + If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.hostnames + name: Hostnames + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: HTTPRoute provides a way to route HTTP requests. This includes + the capability to match requests by hostname, path, header, or query param. + Filters can be used to specify additional processing steps. Backends specify + where matching requests should be routed. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of HTTPRoute. + properties: + hostnames: + description: "Hostnames defines a set of hostname that should match + against the HTTP Host header to select a HTTPRoute to process the + request. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname may + be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n If a hostname is specified + by both the Listener and HTTPRoute, there must be at least one intersecting + hostname for the HTTPRoute to be attached to the Listener. For example: + \n * A Listener with `test.example.com` as the hostname matches + HTTPRoutes that have either not specified any hostnames, or have + specified at least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches HTTPRoutes + \ that have either not specified any hostnames or have specified + at least one hostname that matches the Listener hostname. For + example, `*.example.com`, `test.example.com`, and `foo.test.example.com` + would all match. On the other hand, `example.com` and `test.example.net` + would not match. \n Hostnames that are prefixed with a wildcard + label (`*.`) are interpreted as a suffix match. That means that + a match for `*.example.com` would match both `test.example.com`, + and `foo.test.example.com`, but not `example.com`. \n If both the + Listener and HTTPRoute have specified hostnames, any HTTPRoute hostnames + that do not match the Listener hostname MUST be ignored. For example, + if a Listener specified `*.example.com`, and the HTTPRoute specified + `test.example.com` and `test.example.net`, `test.example.net` must + not be considered for a match. \n If both the Listener and HTTPRoute + have specified hostnames, and none match with the criteria above, + then the HTTPRoute is not accepted. The implementation must raise + an 'Accepted' Condition with a status of `False` in the corresponding + RouteParentStatus. \n Support: Core" + items: + description: "Hostname is the fully qualified domain name of a network + host. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname + may be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n Hostname can be \"precise\" + which is a domain name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", which is a domain + name prefixed with a single wildcard label (e.g. `*.example.com`). + \n Note that as per RFC1035 and RFC1123, a *label* must consist + of lower case alphanumeric characters or '-', and must start and + end with an alphanumeric character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + parentRefs: + description: "ParentRefs references the resources (usually Gateways) + that a Route wants to be attached to. Note that the referenced parent + resource needs to allow this for the attachment to be complete. + For Gateways, that means the Gateway needs to allow attachment from + Routes of this kind and namespace. \n The only kind of parent resource + with \"Core\" support is Gateway. This API may be extended in the + future to support additional kinds of parent resources such as one + of the route kinds. \n It is invalid to reference an identical parent + more than once. It is valid to reference multiple distinct sections + within the same parent resource, such as 2 Listeners within a Gateway. + \n It is possible to separately reference multiple distinct objects + that may be collapsed by an implementation. For example, some implementations + may choose to merge compatible Gateway Listeners together. If that + is the case, the list of routes attached to those resources should + also be merged." + items: + description: "ParentReference identifies an API object (usually + a Gateway) that can be considered a parent of this resource (usually + a route). The only kind of parent resource with \"Core\" support + is Gateway. This API may be extended in the future to support + additional kinds of parent resources, such as HTTPRoute. \n The + API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid." + properties: + group: + default: policy.linkerd.io + description: "Group is the group of the referent. \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: Core + (Gateway) Support: Custom (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. When + unspecified (or empty string), this refers to the local namespace + of the Route. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: "port" + type: integer + sectionName: + description: "SectionName is the name of a section within the + target resource. In the following resources, SectionName is + interpreted as the following: \n * Gateway: Listener Name. + When both Port (experimental) and SectionName are specified, + the name and port of the selected listener must match both + specified values. \n Implementations MAY choose to support + attaching Routes to other resources. If that is the case, + they MUST clearly document how SectionName is interpreted. + \n When unspecified (empty string), this will reference the + entire resource. For the purpose of status, an attachment + is considered successful if at least one section in the parent + resource accepts it. For example, Gateway listeners can restrict + which Routes can attach to them by Route kind, namespace, + or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this + Route, the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + rules: + default: + - matches: + - path: + type: PathPrefix + value: / + description: Rules are a list of HTTP matchers, filters and actions. + items: + description: HTTPRouteRule defines semantics for matching an HTTP + request based on conditions (matches) and processing it (filters). + properties: + backendRefs: + type: array + items: + type: object + properties: + name: + type: string + port: + type: integer + namespace: + type: string + default: "default" + filters: + description: "Filters defined at this level should be + executed if and only if the request is being forwarded + to the backend defined here. \n Support: Implementation-specific + (For broader support of filters, use the Filters field + in HTTPRouteRule.)" + items: + description: HTTPRouteFilter defines processing steps + that must be completed during the request or response + lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway + implementations. Some examples include request or + response modification, implementing authentication + strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type + of the filter. + properties: + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema + for a filter that modifies request headers. \n + Support: Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestRedirect: + description: "RequestRedirect defines a schema for + a filter that responds to the request with an + HTTP redirection. \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be + used in the value of the `Location` header + in the response. When empty, the hostname + in the `Host` header of the request is used. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to + modify the path of the incoming request. The + modified path is then used to construct the + `Location` header. When empty, the request + path is used as-is. \n Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in + the value of the `Location` header in the + response. \n If no port is specified, the + redirect port MUST be derived using the following + rules: \n * If redirect scheme is not-empty, + the redirect port MUST be the well-known port + associated with the redirect scheme. Specifically + \"http\" to port 80 and \"https\" to port + 443. If the redirect scheme does not have + a well-known port, the listener port of the + Gateway SHOULD be used. * If redirect scheme + is empty, the redirect port MUST be the Gateway + Listener port. \n Implementations SHOULD NOT + add the port number in the 'Location' header + in the following cases: \n * A Location header + that will use HTTP (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 80. * A Location header that + will use HTTPS (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 443. \n Support: Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used + in the value of the `Location` header in the + response. When empty, the scheme of the request + is used. \n Scheme redirects can affect the + port of the redirect, for more information, + refer to the documentation for the port field + of this filter. \n Note that values may be + added to this enum, implementations must ensure + that unknown values will not cause a crash. + \n Unknown values here must result in the + implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`. \n Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status + code to be used in response. \n Note that + values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result + in the implementation setting the Accepted + Condition for the Route to `status: False`, + with a Reason of `UnsupportedValue`. \n Support: + Core" + enum: + - 301 + - 302 + type: integer + type: object + responseHeaderModifier: + description: "ResponseHeaderModifier defines a schema + for a filter that modifies response headers. \n + Support: Extended" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: "Type identifies the type of filter + to apply. As with other API fields, types are + classified into three conformance levels: \n - + Core: Filter types and their corresponding configuration + defined by \"Support: Core\" in this package, + e.g. \"RequestHeaderModifier\". All implementations + must support core filters. \n - Extended: Filter + types and their corresponding configuration defined + by \"Support: Extended\" in this package, e.g. + \"RequestMirror\". Implementers are encouraged + to support extended filters. \n - Implementation-specific: + Filters that are defined and supported by specific + vendors. In the future, filters showing convergence + in behavior across multiple implementations will + be considered for inclusion in extended or core + conformance levels. Filter-specific configuration + for such filters is specified using the ExtensionRef + field. `Type` should be set to \"ExtensionRef\" + for custom filters. \n Implementers are encouraged + to define custom implementation types to extend + the core API with implementation-specific behavior. + \n If a reference to a custom filter type cannot + be resolved, the filter MUST NOT be skipped. Instead, + requests that would have been processed by that + filter MUST receive a HTTP error response. \n + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result in + the implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestRedirect + type: string + required: + - type + type: object + maxItems: 16 + type: array + filters: + description: "Filters define the filters that are applied to + requests that match this rule. \n The effects of ordering + of multiple behaviors are currently unspecified. This can + change in the future based on feedback during the alpha stage. + \n Conformance-levels at this level are defined based on the + type of filter: \n - ALL core filters MUST be supported by + all implementations. - Implementers are encouraged to support + extended filters. - Implementation-specific custom filters + have no API guarantees across implementations. \n Specifying + a core filter multiple times has unspecified or custom conformance. + \n All filters are expected to be compatible with each other + except for the URLRewrite and RequestRedirect filters, which + may not be combined. If an implementation can not support + other combinations of filters, they must clearly document + that limitation. In all cases where incompatible or unsupported + filters are specified, implementations MUST add a warning + condition to status. \n Support: Core" + items: + description: HTTPRouteFilter defines processing steps that + must be completed during the request or response lifecycle. + HTTPRouteFilters are meant as an extension point to express + processing that may be done in Gateway implementations. + Some examples include request or response modification, + implementing authentication strategies, rate-limiting, and + traffic shaping. API guarantee/conformance is defined based + on the type of the filter. + properties: + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema for + a filter that modifies request headers. \n Support: + Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It appends + to any existing values associated with the header + name. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: add: - name: \"my-header\" value: + \"bar\" \n Output: GET /foo HTTP/1.1 my-header: + foo my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from the + HTTP request before the action. The value of Remove + is a list of HTTP header names. Note that the header + names are case-insensitive (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + \ my-header2: bar my-header3: baz \n Config: + \ remove: [\"my-header1\", \"my-header3\"] \n Output: + \ GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with the + given header (name, value) before the action. \n + Input: GET /foo HTTP/1.1 my-header: foo \n Config: + \ set: - name: \"my-header\" value: \"bar\" + \n Output: GET /foo HTTP/1.1 my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestRedirect: + description: "RequestRedirect defines a schema for a filter + that responds to the request with an HTTP redirection. + \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be used + in the value of the `Location` header in the response. + When empty, the hostname of the request is used. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to + modify the path of the incoming request. The + modified path is then used to construct the + `Location` header. When empty, the request + path is used as-is. \n Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in the value + of the `Location` header in the response. When empty, + port (if specified) of the request is used. \n Support: + Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used in the + value of the `Location` header in the response. + When empty, the scheme of the request is used. \n + Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status code to + be used in response. \n Support: Core" + enum: + - 301 + - 302 + type: integer + type: object + type: + description: "Type identifies the type of filter to apply. + As with other API fields, types are classified into + three conformance levels: \n - Core: Filter types and + their corresponding configuration defined by \"Support: + Core\" in this package, e.g. \"RequestHeaderModifier\"." + enum: + - RequestHeaderModifier + - RequestRedirect + type: string + required: + - type + type: object + maxItems: 16 + type: array + matches: + default: + - path: + type: PathPrefix + value: / + description: "Matches define conditions used for matching the + rule against incoming HTTP requests. Each match is independent, + i.e. this rule will be matched if **any** one of the matches + is satisfied. \n For example, take the following matches configuration: + \n ``` matches: - path: value: \"/foo\" headers: - + name: \"version\" value: \"v2\" - path: value: \"/v2/foo\" + ``` \n For a request to match against this rule, a request + must satisfy EITHER of the two conditions: \n - path prefixed + with `/foo` AND contains the header `version: v2` - path prefix + of `/v2/foo` \n See the documentation for HTTPRouteMatch on + how to specify multiple match conditions that should be ANDed + together. \n If no matches are specified, the default is a + prefix path match on \"/\", which has the effect of matching + every HTTP request. \n Proxy or Load Balancer routing configuration + generated from HTTPRoutes MUST prioritize rules based on the + following criteria, continuing on ties. Precedence must be + given to the the Rule with the largest number of: \n * Characters + in a matching non-wildcard hostname. * Characters in a matching + hostname. * Characters in a matching path. * Header matches. + * Query param matches. \n If ties still exist across multiple + Routes, matching precedence MUST be determined in order of + the following criteria, continuing on ties: \n * The oldest + Route based on creation timestamp. * The Route appearing first + in alphabetical order by \"{namespace}/{name}\". \n If ties + still exist within the Route that has been given precedence, + matching precedence MUST be granted to the first matching + rule meeting the above criteria. \n When no rules matching + a request have been successfully attached to the parent a + request is coming from, a HTTP 404 status code MUST be returned." + items: + description: "HTTPRouteMatch defines the predicate used to + match requests to a given action. Multiple match types are + ANDed together, i.e. the match will evaluate to true only + if all conditions are satisfied. \n For example, the match + below will match a HTTP request only if its path starts + with `/foo` AND it contains the `version: v1` header: \n + ``` match: path: value: \"/foo\" headers: - name: + \"version\" value \"v1\" ```" + properties: + headers: + description: Headers specifies HTTP request header matchers. + Multiple match values are ANDed together, meaning, a + request must match all the specified headers to select + the route. + items: + description: HTTPHeaderMatch describes how to select + a HTTP route by matching HTTP request headers. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case insensitive. + (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent header + names, only the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST be + ignored. Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered equivalent. + \n When a header is repeated in an HTTP request, + it is implementation-specific behavior as to how + this is represented. Generally, proxies should + follow the guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, with special + handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the header. \n Support: Core (Exact) + \n Support: Custom (RegularExpression) \n Since + RegularExpression HeaderMatchType has custom conformance, + implementations can support POSIX, PCRE or any + other dialects of regular expressions. Please + read the implementation's documentation to determine + the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP Header to + be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. When + specified, this route will be matched only if the request + has the specified method. \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path matcher. + If this field is not specified, a default prefix match + on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match against + the path Value. \n Support: Core (Exact, PathPrefix) + \n Support: Custom (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match against. + maxLength: 1024 + type: string + type: object + queryParams: + description: QueryParams specifies HTTP query parameter + matchers. Multiple match values are ANDed together, + meaning, a request must match all the specified query + parameters to select the route. + items: + description: HTTPQueryParamMatch describes how to select + a HTTP route by matching HTTP query parameters. + properties: + name: + description: Name is the name of the HTTP query + param to be matched. This must be an exact string + match. (See https://tools.ietf.org/html/rfc7230#section-2.7.3). + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the query parameter. \n Support: + Extended (Exact) \n Support: Custom (RegularExpression) + \n Since RegularExpression QueryParamMatchType + has custom conformance, implementations can support + POSIX, PCRE or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP query param + to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + maxItems: 8 + type: array + type: object + maxItems: 16 + type: array + type: object + status: + description: Status defines the current state of HTTPRoute. + properties: + parents: + description: "Parents is a list of parent resources (usually Gateways) + that are associated with the route, and the status of the route + with respect to each parent. When this route attaches to a parent, + the controller that manages the parent must add an entry to this + list when the controller first sees the route and should update + the entry as appropriate when the route or gateway is modified. + \n Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this + API can only populate Route status for the Gateways/parent resources + they are responsible for. \n A maximum of 32 Gateways will be represented + in this list. An empty list means the route has not been attached + to any Gateway." + items: + description: RouteParentStatus describes the status of a route with + respect to an associated Parent. + properties: + conditions: + description: "Conditions describes the status of the route with + respect to the Gateway. Note that the route's availability + is also subject to the Gateway's own status conditions and + listener status. \n If the Route's ParentRef specifies an + existing Gateway that supports Routes of this kind AND that + Gateway's controller has sufficient access, then that Gateway's + controller MUST set the \"Accepted\" condition on the Route, + to indicate whether the route has been accepted or rejected + by the Gateway, and why. \n A Route MUST be considered \"Accepted\" + if at least one of the Route's rules is implemented by the + Gateway. \n There are a number of cases where the \"Accepted\" + condition may not be set due to lack of controller visibility, + that includes when: \n * The Route refers to a non-existent + parent. * The Route is of a type that the controller does + not support. * The Route is in a namespace the the controller + does not have access to." + items: + description: "Condition contains details for one aspect of + the current state of this API Resource. --- This struct + is intended for direct use as an array at the field path + .status.conditions. For example, type FooStatus struct{ + \ // Represents the observations of a foo's current state. + \ // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type + \ // +patchStrategy=merge // +listType=map // + +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` + \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should + be when the underlying condition changed. If that is + not known, then using the time when the API field changed + is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, + if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the + current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier + indicating the reason for the condition's last transition. + Producers of specific condition types may define expected + values and meanings for this field, and whether the + values are considered a guaranteed API. The value should + be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across + resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability + to deconflict is important. The regex it matches is + (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: "ControllerName is a domain/path string that indicates + the name of the controller that wrote this status. This corresponds + with the controllerName field on GatewayClass. \n Example: + \"example.net/gateway-controller\". \n The format of this + field is DOMAIN \"/\" PATH, where DOMAIN and PATH are valid + Kubernetes names (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + \n Controllers MUST populate this field when writing status. + Controllers should ensure that entries to status populated + with their ControllerName are cleaned up when they are no + longer necessary." + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: ParentRef corresponds with a ParentRef in the spec + that this RouteParentStatus struct describes the status of. + properties: + group: + default: policy.linkerd.io + description: "Group is the group of the referent. \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: + Core (Gateway) Support: Custom (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. + When unspecified (or empty string), this refers to the + local namespace of the Route. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + sectionName: + description: "SectionName is the name of a section within + the target resource. In the following resources, SectionName + is interpreted as the following: \n * Gateway: Listener + Name. When both Port (experimental) and SectionName are + specified, the name and port of the selected listener + must match both specified values. \n Implementations MAY + choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName + is interpreted. \n When unspecified (empty string), this + will reference the entire resource. For the purpose of + status, an attachment is considered successful if at least + one section in the parent resource accepts it. For example, + Gateway listeners can restrict which Routes can attach + to them by Route kind, namespace, or hostname. If 1 of + 2 Gateway listeners accept attachment from the referencing + Route, the Route MUST be considered successfully attached. + If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.hostnames + name: Hostnames + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta2 + schema: + openAPIV3Schema: + description: HTTPRoute provides a way to route HTTP requests. This includes + the capability to match requests by hostname, path, header, or query param. + Filters can be used to specify additional processing steps. Backends specify + where matching requests should be routed. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of HTTPRoute. + properties: + hostnames: + description: "Hostnames defines a set of hostname that should match + against the HTTP Host header to select a HTTPRoute to process the + request. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname may + be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n If a hostname is specified + by both the Listener and HTTPRoute, there must be at least one intersecting + hostname for the HTTPRoute to be attached to the Listener. For example: + \n * A Listener with `test.example.com` as the hostname matches + HTTPRoutes that have either not specified any hostnames, or have + specified at least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches HTTPRoutes + \ that have either not specified any hostnames or have specified + at least one hostname that matches the Listener hostname. For + example, `*.example.com`, `test.example.com`, and `foo.test.example.com` + would all match. On the other hand, `example.com` and `test.example.net` + would not match. \n Hostnames that are prefixed with a wildcard + label (`*.`) are interpreted as a suffix match. That means that + a match for `*.example.com` would match both `test.example.com`, + and `foo.test.example.com`, but not `example.com`. \n If both the + Listener and HTTPRoute have specified hostnames, any HTTPRoute hostnames + that do not match the Listener hostname MUST be ignored. For example, + if a Listener specified `*.example.com`, and the HTTPRoute specified + `test.example.com` and `test.example.net`, `test.example.net` must + not be considered for a match. \n If both the Listener and HTTPRoute + have specified hostnames, and none match with the criteria above, + then the HTTPRoute is not accepted. The implementation must raise + an 'Accepted' Condition with a status of `False` in the corresponding + RouteParentStatus. \n Support: Core" + items: + description: "Hostname is the fully qualified domain name of a network + host. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname + may be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n Hostname can be \"precise\" + which is a domain name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", which is a domain + name prefixed with a single wildcard label (e.g. `*.example.com`). + \n Note that as per RFC1035 and RFC1123, a *label* must consist + of lower case alphanumeric characters or '-', and must start and + end with an alphanumeric character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + parentRefs: + description: "ParentRefs references the resources (usually Gateways) + that a Route wants to be attached to. Note that the referenced parent + resource needs to allow this for the attachment to be complete. + For Gateways, that means the Gateway needs to allow attachment from + Routes of this kind and namespace. \n The only kind of parent resource + with \"Core\" support is Gateway. This API may be extended in the + future to support additional kinds of parent resources such as one + of the route kinds. \n It is invalid to reference an identical parent + more than once. It is valid to reference multiple distinct sections + within the same parent resource, such as 2 Listeners within a Gateway. + \n It is possible to separately reference multiple distinct objects + that may be collapsed by an implementation. For example, some implementations + may choose to merge compatible Gateway Listeners together. If that + is the case, the list of routes attached to those resources should + also be merged." + items: + description: "ParentReference identifies an API object (usually + a Gateway) that can be considered a parent of this resource (usually + a route). The only kind of parent resource with \"Core\" support + is Gateway. This API may be extended in the future to support + additional kinds of parent resources, such as HTTPRoute. \n The + API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid." + properties: + group: + default: policy.linkerd.io + description: "Group is the group of the referent. \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: Core + (Gateway) Support: Custom (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. When + unspecified (or empty string), this refers to the local namespace + of the Route. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: "Port specifies the destination + port number to use for this resource. + Port is required when the referent is + a Kubernetes Service. In this case, the + port number is the service port number, + not the target port. For other resources, + destination port might be derived from + the referent resource or this field. \n Support: Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: "SectionName is the name of a section within the + target resource. In the following resources, SectionName is + interpreted as the following: \n * Gateway: Listener Name. + When both Port (experimental) and SectionName are specified, + the name and port of the selected listener must match both + specified values. \n Implementations MAY choose to support + attaching Routes to other resources. If that is the case, + they MUST clearly document how SectionName is interpreted. + \n When unspecified (empty string), this will reference the + entire resource. For the purpose of status, an attachment + is considered successful if at least one section in the parent + resource accepts it. For example, Gateway listeners can restrict + which Routes can attach to them by Route kind, namespace, + or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this + Route, the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + rules: + default: + - matches: + - path: + type: PathPrefix + value: / + description: Rules are a list of HTTP matchers, filters and actions. + items: + description: HTTPRouteRule defines semantics for matching an HTTP + request based on conditions (matches) and processing it (filters). + properties: + backendRefs: + description: "BackendRefs defines the backend(s) where matching + requests should be sent. \n Failure behavior here depends + on how many BackendRefs are specified and how many are invalid. + \n If *all* entries in BackendRefs are invalid, and there + are also no filters specified in this route rule, *all* traffic + which matches this rule MUST receive a 500 status code. \n + See the HTTPBackendRef definition for the rules about what + makes a single HTTPBackendRef invalid. \n When a HTTPBackendRef + is invalid, 500 status codes MUST be returned for requests + that would have otherwise been routed to an invalid backend. + If multiple backends are specified, and some are invalid, + the proportion of requests that would otherwise have been + routed to an invalid backend MUST receive a 500 status code. + \n For example, if two backends are specified with equal weights, + and one is invalid, 50 percent of traffic must receive a 500. + Implementations may choose how that 50 percent is determined. + \n Support: Core for Kubernetes Service \n Support: Implementation-specific + for any other resource \n Support for weight: Core" + items: + description: HTTPBackendRef defines how a HTTPRoute should + forward an HTTP request. + properties: + group: + default: "" + description: Group is the group of the referent. For example, + "gateway.networking.k8s.io". When unspecified or empty + string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: Kind is kind of the referent. For example + "HTTPRoute" or "Service". Defaults to "Service" when + not specified. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the backend. + When unspecified, the local namespace is inferred. \n + Note that when a namespace is specified, a ReferenceGrant + object is required in the referent namespace to allow + that namespace's owner to accept the reference. See + the ReferenceGrant documentation for details. \n Support: + Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: Port specifies the destination port number + to use for this resource. Port is required when the + referent is a Kubernetes Service. In this case, the + port number is the service port number, not the target + port. For other resources, destination port might be + derived from the referent resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: "Weight specifies the proportion of requests + forwarded to the referenced backend. This is computed + as weight/(sum of all weights in this BackendRefs list). + For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision + an implementation supports. Weight is not a percentage + and the sum of weights does not need to equal 100. \n + If only one backend is specified and it has a weight + greater than 0, 100% of the traffic is forwarded to + that backend. If weight is set to 0, no traffic should + be forwarded for this entry. If unspecified, weight + defaults to 1. \n Support for this field varies based + on the context where used." + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + filters: + description: "Filters defined at this level should be + executed if and only if the request is being forwarded + to the backend defined here. \n Support: Implementation-specific + (For broader support of filters, use the Filters field + in HTTPRouteRule.)" + items: + description: HTTPRouteFilter defines processing steps + that must be completed during the request or response + lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway + implementations. Some examples include request or + response modification, implementing authentication + strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type + of the filter. + properties: + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema + for a filter that modifies request headers. \n + Support: Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestRedirect: + description: "RequestRedirect defines a schema for + a filter that responds to the request with an + HTTP redirection. \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be + used in the value of the `Location` header + in the response. When empty, the hostname + in the `Host` header of the request is used. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to + modify the path of the incoming request. The + modified path is then used to construct the + `Location` header. When empty, the request + path is used as-is. \n Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in + the value of the `Location` header in the + response. \n If no port is specified, the + redirect port MUST be derived using the following + rules: \n * If redirect scheme is not-empty, + the redirect port MUST be the well-known port + associated with the redirect scheme. Specifically + \"http\" to port 80 and \"https\" to port + 443. If the redirect scheme does not have + a well-known port, the listener port of the + Gateway SHOULD be used. * If redirect scheme + is empty, the redirect port MUST be the Gateway + Listener port. \n Implementations SHOULD NOT + add the port number in the 'Location' header + in the following cases: \n * A Location header + that will use HTTP (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 80. * A Location header that + will use HTTPS (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 443. \n Support: Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used + in the value of the `Location` header in the + response. When empty, the scheme of the request + is used. \n Scheme redirects can affect the + port of the redirect, for more information, + refer to the documentation for the port field + of this filter. \n Note that values may be + added to this enum, implementations must ensure + that unknown values will not cause a crash. + \n Unknown values here must result in the + implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`. \n Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status + code to be used in response. \n Note that + values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result + in the implementation setting the Accepted + Condition for the Route to `status: False`, + with a Reason of `UnsupportedValue`. \n Support: + Core" + enum: + - 301 + - 302 + type: integer + type: object + responseHeaderModifier: + description: "ResponseHeaderModifier defines a schema + for a filter that modifies response headers. \n + Support: Extended" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: "Type identifies the type of filter + to apply. As with other API fields, types are + classified into three conformance levels: \n - + Core: Filter types and their corresponding configuration + defined by \"Support: Core\" in this package, + e.g. \"RequestHeaderModifier\". All implementations + must support core filters. \n - Extended: Filter + types and their corresponding configuration defined + by \"Support: Extended\" in this package, e.g. + \"RequestMirror\". Implementers are encouraged + to support extended filters. \n - Implementation-specific: + Filters that are defined and supported by specific + vendors. In the future, filters showing convergence + in behavior across multiple implementations will + be considered for inclusion in extended or core + conformance levels. Filter-specific configuration + for such filters is specified using the ExtensionRef + field. `Type` should be set to \"ExtensionRef\" + for custom filters. \n Implementers are encouraged + to define custom implementation types to extend + the core API with implementation-specific behavior. + \n If a reference to a custom filter type cannot + be resolved, the filter MUST NOT be skipped. Instead, + requests that would have been processed by that + filter MUST receive a HTTP error response. \n + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result in + the implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestRedirect + type: string + required: + - type + type: object + maxItems: 16 + type: array + required: + - name + type: object + maxItems: 16 + type: array + filters: + description: "Filters define the filters that are applied to + requests that match this rule. \n The effects of ordering + of multiple behaviors are currently unspecified. This can + change in the future based on feedback during the alpha stage. + \n Conformance-levels at this level are defined based on the + type of filter: \n - ALL core filters MUST be supported by + all implementations. - Implementers are encouraged to support + extended filters. - Implementation-specific custom filters + have no API guarantees across implementations. \n Specifying + a core filter multiple times has unspecified or custom conformance. + \n All filters are expected to be compatible with each other + except for the URLRewrite and RequestRedirect filters, which + may not be combined. If an implementation can not support + other combinations of filters, they must clearly document + that limitation. In all cases where incompatible or unsupported + filters are specified, implementations MUST add a warning + condition to status. \n Support: Core" + items: + description: HTTPRouteFilter defines processing steps that + must be completed during the request or response lifecycle. + HTTPRouteFilters are meant as an extension point to express + processing that may be done in Gateway implementations. + Some examples include request or response modification, + implementing authentication strategies, rate-limiting, and + traffic shaping. API guarantee/conformance is defined based + on the type of the filter. + properties: + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema for + a filter that modifies request headers. \n Support: + Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It appends + to any existing values associated with the header + name. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: add: - name: \"my-header\" value: + \"bar\" \n Output: GET /foo HTTP/1.1 my-header: + foo my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from the + HTTP request before the action. The value of Remove + is a list of HTTP header names. Note that the header + names are case-insensitive (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + \ my-header2: bar my-header3: baz \n Config: + \ remove: [\"my-header1\", \"my-header3\"] \n Output: + \ GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with the + given header (name, value) before the action. \n + Input: GET /foo HTTP/1.1 my-header: foo \n Config: + \ set: - name: \"my-header\" value: \"bar\" + \n Output: GET /foo HTTP/1.1 my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestRedirect: + description: "RequestRedirect defines a schema for a filter + that responds to the request with an HTTP redirection. + \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be used + in the value of the `Location` header in the response. + When empty, the hostname of the request is used. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to + modify the path of the incoming request. The + modified path is then used to construct the + `Location` header. When empty, the request + path is used as-is. \n Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in the value + of the `Location` header in the response. When empty, + port (if specified) of the request is used. \n Support: + Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used in the + value of the `Location` header in the response. + When empty, the scheme of the request is used. \n + Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status code to + be used in response. \n Support: Core" + enum: + - 301 + - 302 + type: integer + type: object + type: + description: "Type identifies the type of filter to apply. + As with other API fields, types are classified into + three conformance levels: \n - Core: Filter types and + their corresponding configuration defined by \"Support: + Core\" in this package, e.g. \"RequestHeaderModifier\"." + enum: + - RequestHeaderModifier + - RequestRedirect + type: string + required: + - type + type: object + maxItems: 16 + type: array + matches: + default: + - path: + type: PathPrefix + value: / + description: "Matches define conditions used for matching the + rule against incoming HTTP requests. Each match is independent, + i.e. this rule will be matched if **any** one of the matches + is satisfied. \n For example, take the following matches configuration: + \n ``` matches: - path: value: \"/foo\" headers: - + name: \"version\" value: \"v2\" - path: value: \"/v2/foo\" + ``` \n For a request to match against this rule, a request + must satisfy EITHER of the two conditions: \n - path prefixed + with `/foo` AND contains the header `version: v2` - path prefix + of `/v2/foo` \n See the documentation for HTTPRouteMatch on + how to specify multiple match conditions that should be ANDed + together. \n If no matches are specified, the default is a + prefix path match on \"/\", which has the effect of matching + every HTTP request. \n Proxy or Load Balancer routing configuration + generated from HTTPRoutes MUST prioritize rules based on the + following criteria, continuing on ties. Precedence must be + given to the the Rule with the largest number of: \n * Characters + in a matching non-wildcard hostname. * Characters in a matching + hostname. * Characters in a matching path. * Header matches. + * Query param matches. \n If ties still exist across multiple + Routes, matching precedence MUST be determined in order of + the following criteria, continuing on ties: \n * The oldest + Route based on creation timestamp. * The Route appearing first + in alphabetical order by \"{namespace}/{name}\". \n If ties + still exist within the Route that has been given precedence, + matching precedence MUST be granted to the first matching + rule meeting the above criteria. \n When no rules matching + a request have been successfully attached to the parent a + request is coming from, a HTTP 404 status code MUST be returned." + items: + description: "HTTPRouteMatch defines the predicate used to + match requests to a given action. Multiple match types are + ANDed together, i.e. the match will evaluate to true only + if all conditions are satisfied. \n For example, the match + below will match a HTTP request only if its path starts + with `/foo` AND it contains the `version: v1` header: \n + ``` match: path: value: \"/foo\" headers: - name: + \"version\" value \"v1\" ```" + properties: + headers: + description: Headers specifies HTTP request header matchers. + Multiple match values are ANDed together, meaning, a + request must match all the specified headers to select + the route. + items: + description: HTTPHeaderMatch describes how to select + a HTTP route by matching HTTP request headers. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case insensitive. + (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent header + names, only the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST be + ignored. Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered equivalent. + \n When a header is repeated in an HTTP request, + it is implementation-specific behavior as to how + this is represented. Generally, proxies should + follow the guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, with special + handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the header. \n Support: Core (Exact) + \n Support: Custom (RegularExpression) \n Since + RegularExpression HeaderMatchType has custom conformance, + implementations can support POSIX, PCRE or any + other dialects of regular expressions. Please + read the implementation's documentation to determine + the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP Header to + be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. When + specified, this route will be matched only if the request + has the specified method. \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path matcher. + If this field is not specified, a default prefix match + on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match against + the path Value. \n Support: Core (Exact, PathPrefix) + \n Support: Custom (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match against. + maxLength: 1024 + type: string + type: object + queryParams: + description: QueryParams specifies HTTP query parameter + matchers. Multiple match values are ANDed together, + meaning, a request must match all the specified query + parameters to select the route. + items: + description: HTTPQueryParamMatch describes how to select + a HTTP route by matching HTTP query parameters. + properties: + name: + description: Name is the name of the HTTP query + param to be matched. This must be an exact string + match. (See https://tools.ietf.org/html/rfc7230#section-2.7.3). + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the query parameter. \n Support: + Extended (Exact) \n Support: Custom (RegularExpression) + \n Since RegularExpression QueryParamMatchType + has custom conformance, implementations can support + POSIX, PCRE or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP query param + to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + maxItems: 8 + type: array + type: object + maxItems: 16 + type: array + type: object + status: + description: Status defines the current state of HTTPRoute. + properties: + parents: + description: "Parents is a list of parent resources (usually Gateways) + that are associated with the route, and the status of the route + with respect to each parent. When this route attaches to a parent, + the controller that manages the parent must add an entry to this + list when the controller first sees the route and should update + the entry as appropriate when the route or gateway is modified. + \n Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this + API can only populate Route status for the Gateways/parent resources + they are responsible for. \n A maximum of 32 Gateways will be represented + in this list. An empty list means the route has not been attached + to any Gateway." + items: + description: RouteParentStatus describes the status of a route with + respect to an associated Parent. + properties: + conditions: + description: "Conditions describes the status of the route with + respect to the Gateway. Note that the route's availability + is also subject to the Gateway's own status conditions and + listener status. \n If the Route's ParentRef specifies an + existing Gateway that supports Routes of this kind AND that + Gateway's controller has sufficient access, then that Gateway's + controller MUST set the \"Accepted\" condition on the Route, + to indicate whether the route has been accepted or rejected + by the Gateway, and why. \n A Route MUST be considered \"Accepted\" + if at least one of the Route's rules is implemented by the + Gateway. \n There are a number of cases where the \"Accepted\" + condition may not be set due to lack of controller visibility, + that includes when: \n * The Route refers to a non-existent + parent. * The Route is of a type that the controller does + not support. * The Route is in a namespace the the controller + does not have access to." + items: + description: "Condition contains details for one aspect of + the current state of this API Resource. --- This struct + is intended for direct use as an array at the field path + .status.conditions. For example, type FooStatus struct{ + \ // Represents the observations of a foo's current state. + \ // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type + \ // +patchStrategy=merge // +listType=map // + +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` + \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should + be when the underlying condition changed. If that is + not known, then using the time when the API field changed + is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, + if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the + current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier + indicating the reason for the condition's last transition. + Producers of specific condition types may define expected + values and meanings for this field, and whether the + values are considered a guaranteed API. The value should + be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across + resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability + to deconflict is important. The regex it matches is + (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: "ControllerName is a domain/path string that indicates + the name of the controller that wrote this status. This corresponds + with the controllerName field on GatewayClass. \n Example: + \"example.net/gateway-controller\". \n The format of this + field is DOMAIN \"/\" PATH, where DOMAIN and PATH are valid + Kubernetes names (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + \n Controllers MUST populate this field when writing status. + Controllers should ensure that entries to status populated + with their ControllerName are cleaned up when they are no + longer necessary." + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: ParentRef corresponds with a ParentRef in the spec + that this RouteParentStatus struct describes the status of. + properties: + group: + default: policy.linkerd.io + description: "Group is the group of the referent. \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: + Core (Gateway) Support: Custom (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. + When unspecified (or empty string), this refers to the + local namespace of the Route. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + sectionName: + description: "SectionName is the name of a section within + the target resource. In the following resources, SectionName + is interpreted as the following: \n * Gateway: Listener + Name. When both Port (experimental) and SectionName are + specified, the name and port of the selected listener + must match both specified values. \n Implementations MAY + choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName + is interpreted. \n When unspecified (empty string), this + will reference the entire resource. For the purpose of + status, an attachment is considered successful if at least + one section in the parent resource accepts it. For example, + Gateway listeners can restrict which Routes can attach + to them by Route kind, namespace, or hostname. If 1 of + 2 Gateway listeners accept attachment from the referencing + Route, the Route MUST be considered successfully attached. + If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.hostnames + name: Hostnames + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta3 + schema: + openAPIV3Schema: + description: HTTPRoute provides a way to route HTTP requests. This includes + the capability to match requests by hostname, path, header, or query param. + Filters can be used to specify additional processing steps. Backends specify + where matching requests should be routed. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of HTTPRoute. + properties: + hostnames: + description: "Hostnames defines a set of hostname that should match + against the HTTP Host header to select a HTTPRoute to process the + request. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname may + be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n If a hostname is specified + by both the Listener and HTTPRoute, there must be at least one intersecting + hostname for the HTTPRoute to be attached to the Listener. For example: + \n * A Listener with `test.example.com` as the hostname matches + HTTPRoutes that have either not specified any hostnames, or have + specified at least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches HTTPRoutes + \ that have either not specified any hostnames or have specified + at least one hostname that matches the Listener hostname. For + example, `*.example.com`, `test.example.com`, and `foo.test.example.com` + would all match. On the other hand, `example.com` and `test.example.net` + would not match. \n Hostnames that are prefixed with a wildcard + label (`*.`) are interpreted as a suffix match. That means that + a match for `*.example.com` would match both `test.example.com`, + and `foo.test.example.com`, but not `example.com`. \n If both the + Listener and HTTPRoute have specified hostnames, any HTTPRoute hostnames + that do not match the Listener hostname MUST be ignored. For example, + if a Listener specified `*.example.com`, and the HTTPRoute specified + `test.example.com` and `test.example.net`, `test.example.net` must + not be considered for a match. \n If both the Listener and HTTPRoute + have specified hostnames, and none match with the criteria above, + then the HTTPRoute is not accepted. The implementation must raise + an 'Accepted' Condition with a status of `False` in the corresponding + RouteParentStatus. \n Support: Core" + items: + description: "Hostname is the fully qualified domain name of a network + host. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname + may be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n Hostname can be \"precise\" + which is a domain name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", which is a domain + name prefixed with a single wildcard label (e.g. `*.example.com`). + \n Note that as per RFC1035 and RFC1123, a *label* must consist + of lower case alphanumeric characters or '-', and must start and + end with an alphanumeric character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + parentRefs: + description: "ParentRefs references the resources (usually Gateways) + that a Route wants to be attached to. Note that the referenced parent + resource needs to allow this for the attachment to be complete. + For Gateways, that means the Gateway needs to allow attachment from + Routes of this kind and namespace. \n The only kind of parent resource + with \"Core\" support is Gateway. This API may be extended in the + future to support additional kinds of parent resources such as one + of the route kinds. \n It is invalid to reference an identical parent + more than once. It is valid to reference multiple distinct sections + within the same parent resource, such as 2 Listeners within a Gateway. + \n It is possible to separately reference multiple distinct objects + that may be collapsed by an implementation. For example, some implementations + may choose to merge compatible Gateway Listeners together. If that + is the case, the list of routes attached to those resources should + also be merged." + items: + description: "ParentReference identifies an API object (usually + a Gateway) that can be considered a parent of this resource (usually + a route). The only kind of parent resource with \"Core\" support + is Gateway. This API may be extended in the future to support + additional kinds of parent resources, such as HTTPRoute. \n The + API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid." + properties: + group: + default: policy.linkerd.io + description: "Group is the group of the referent. \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: Core + (Gateway) Support: Custom (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. When + unspecified (or empty string), this refers to the local namespace + of the Route. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: "Port specifies the destination + port number to use for this resource. + Port is required when the referent is + a Kubernetes Service. In this case, the + port number is the service port number, + not the target port. For other resources, + destination port might be derived from + the referent resource or this field. \n Support: Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: "SectionName is the name of a section within the + target resource. In the following resources, SectionName is + interpreted as the following: \n * Gateway: Listener Name. + When both Port (experimental) and SectionName are specified, + the name and port of the selected listener must match both + specified values. \n Implementations MAY choose to support + attaching Routes to other resources. If that is the case, + they MUST clearly document how SectionName is interpreted. + \n When unspecified (empty string), this will reference the + entire resource. For the purpose of status, an attachment + is considered successful if at least one section in the parent + resource accepts it. For example, Gateway listeners can restrict + which Routes can attach to them by Route kind, namespace, + or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this + Route, the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + rules: + default: + - matches: + - path: + type: PathPrefix + value: / + description: Rules are a list of HTTP matchers, filters and actions. + items: + description: HTTPRouteRule defines semantics for matching an HTTP + request based on conditions (matches) and processing it (filters). + properties: + backendRefs: + description: "BackendRefs defines the backend(s) where matching + requests should be sent. \n Failure behavior here depends + on how many BackendRefs are specified and how many are invalid. + \n If *all* entries in BackendRefs are invalid, and there + are also no filters specified in this route rule, *all* traffic + which matches this rule MUST receive a 500 status code. \n + See the HTTPBackendRef definition for the rules about what + makes a single HTTPBackendRef invalid. \n When a HTTPBackendRef + is invalid, 500 status codes MUST be returned for requests + that would have otherwise been routed to an invalid backend. + If multiple backends are specified, and some are invalid, + the proportion of requests that would otherwise have been + routed to an invalid backend MUST receive a 500 status code. + \n For example, if two backends are specified with equal weights, + and one is invalid, 50 percent of traffic must receive a 500. + Implementations may choose how that 50 percent is determined. + \n Support: Core for Kubernetes Service \n Support: Implementation-specific + for any other resource \n Support for weight: Core" + items: + description: HTTPBackendRef defines how a HTTPRoute should + forward an HTTP request. + properties: + group: + default: "" + description: Group is the group of the referent. For example, + "gateway.networking.k8s.io". When unspecified or empty + string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: Kind is kind of the referent. For example + "HTTPRoute" or "Service". Defaults to "Service" when + not specified. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the backend. + When unspecified, the local namespace is inferred. \n + Note that when a namespace is specified, a ReferenceGrant + object is required in the referent namespace to allow + that namespace's owner to accept the reference. See + the ReferenceGrant documentation for details. \n Support: + Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: Port specifies the destination port number + to use for this resource. Port is required when the + referent is a Kubernetes Service. In this case, the + port number is the service port number, not the target + port. For other resources, destination port might be + derived from the referent resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: "Weight specifies the proportion of requests + forwarded to the referenced backend. This is computed + as weight/(sum of all weights in this BackendRefs list). + For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision + an implementation supports. Weight is not a percentage + and the sum of weights does not need to equal 100. \n + If only one backend is specified and it has a weight + greater than 0, 100% of the traffic is forwarded to + that backend. If weight is set to 0, no traffic should + be forwarded for this entry. If unspecified, weight + defaults to 1. \n Support for this field varies based + on the context where used." + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + filters: + description: "Filters defined at this level should be + executed if and only if the request is being forwarded + to the backend defined here. \n Support: Implementation-specific + (For broader support of filters, use the Filters field + in HTTPRouteRule.)" + items: + description: HTTPRouteFilter defines processing steps + that must be completed during the request or response + lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway + implementations. Some examples include request or + response modification, implementing authentication + strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type + of the filter. + properties: + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema + for a filter that modifies request headers. \n + Support: Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestRedirect: + description: "RequestRedirect defines a schema for + a filter that responds to the request with an + HTTP redirection. \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be + used in the value of the `Location` header + in the response. When empty, the hostname + in the `Host` header of the request is used. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to + modify the path of the incoming request. The + modified path is then used to construct the + `Location` header. When empty, the request + path is used as-is. \n Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in + the value of the `Location` header in the + response. \n If no port is specified, the + redirect port MUST be derived using the following + rules: \n * If redirect scheme is not-empty, + the redirect port MUST be the well-known port + associated with the redirect scheme. Specifically + \"http\" to port 80 and \"https\" to port + 443. If the redirect scheme does not have + a well-known port, the listener port of the + Gateway SHOULD be used. * If redirect scheme + is empty, the redirect port MUST be the Gateway + Listener port. \n Implementations SHOULD NOT + add the port number in the 'Location' header + in the following cases: \n * A Location header + that will use HTTP (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 80. * A Location header that + will use HTTPS (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 443. \n Support: Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used + in the value of the `Location` header in the + response. When empty, the scheme of the request + is used. \n Scheme redirects can affect the + port of the redirect, for more information, + refer to the documentation for the port field + of this filter. \n Note that values may be + added to this enum, implementations must ensure + that unknown values will not cause a crash. + \n Unknown values here must result in the + implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`. \n Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status + code to be used in response. \n Note that + values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result + in the implementation setting the Accepted + Condition for the Route to `status: False`, + with a Reason of `UnsupportedValue`. \n Support: + Core" + enum: + - 301 + - 302 + type: integer + type: object + responseHeaderModifier: + description: "ResponseHeaderModifier defines a schema + for a filter that modifies response headers. \n + Support: Extended" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: "Type identifies the type of filter + to apply. As with other API fields, types are + classified into three conformance levels: \n - + Core: Filter types and their corresponding configuration + defined by \"Support: Core\" in this package, + e.g. \"RequestHeaderModifier\". All implementations + must support core filters. \n - Extended: Filter + types and their corresponding configuration defined + by \"Support: Extended\" in this package, e.g. + \"RequestMirror\". Implementers are encouraged + to support extended filters. \n - Implementation-specific: + Filters that are defined and supported by specific + vendors. In the future, filters showing convergence + in behavior across multiple implementations will + be considered for inclusion in extended or core + conformance levels. Filter-specific configuration + for such filters is specified using the ExtensionRef + field. `Type` should be set to \"ExtensionRef\" + for custom filters. \n Implementers are encouraged + to define custom implementation types to extend + the core API with implementation-specific behavior. + \n If a reference to a custom filter type cannot + be resolved, the filter MUST NOT be skipped. Instead, + requests that would have been processed by that + filter MUST receive a HTTP error response. \n + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result in + the implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestRedirect + type: string + required: + - type + type: object + maxItems: 16 + type: array + required: + - name + type: object + maxItems: 16 + type: array + filters: + description: "Filters define the filters that are applied to + requests that match this rule. \n The effects of ordering + of multiple behaviors are currently unspecified. This can + change in the future based on feedback during the alpha stage. + \n Conformance-levels at this level are defined based on the + type of filter: \n - ALL core filters MUST be supported by + all implementations. - Implementers are encouraged to support + extended filters. - Implementation-specific custom filters + have no API guarantees across implementations. \n Specifying + a core filter multiple times has unspecified or custom conformance. + \n All filters are expected to be compatible with each other + except for the URLRewrite and RequestRedirect filters, which + may not be combined. If an implementation can not support + other combinations of filters, they must clearly document + that limitation. In all cases where incompatible or unsupported + filters are specified, implementations MUST add a warning + condition to status. \n Support: Core" + items: + description: HTTPRouteFilter defines processing steps that + must be completed during the request or response lifecycle. + HTTPRouteFilters are meant as an extension point to express + processing that may be done in Gateway implementations. + Some examples include request or response modification, + implementing authentication strategies, rate-limiting, and + traffic shaping. API guarantee/conformance is defined based + on the type of the filter. + properties: + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema for + a filter that modifies request headers. \n Support: + Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It appends + to any existing values associated with the header + name. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: add: - name: \"my-header\" value: + \"bar\" \n Output: GET /foo HTTP/1.1 my-header: + foo my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from the + HTTP request before the action. The value of Remove + is a list of HTTP header names. Note that the header + names are case-insensitive (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + \ my-header2: bar my-header3: baz \n Config: + \ remove: [\"my-header1\", \"my-header3\"] \n Output: + \ GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with the + given header (name, value) before the action. \n + Input: GET /foo HTTP/1.1 my-header: foo \n Config: + \ set: - name: \"my-header\" value: \"bar\" + \n Output: GET /foo HTTP/1.1 my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestRedirect: + description: "RequestRedirect defines a schema for a filter + that responds to the request with an HTTP redirection. + \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be used + in the value of the `Location` header in the response. + When empty, the hostname of the request is used. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to + modify the path of the incoming request. The + modified path is then used to construct the + `Location` header. When empty, the request + path is used as-is. \n Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in the value + of the `Location` header in the response. When empty, + port (if specified) of the request is used. \n Support: + Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used in the + value of the `Location` header in the response. + When empty, the scheme of the request is used. \n + Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status code to + be used in response. \n Support: Core" + enum: + - 301 + - 302 + type: integer + type: object + type: + description: "Type identifies the type of filter to apply. + As with other API fields, types are classified into + three conformance levels: \n - Core: Filter types and + their corresponding configuration defined by \"Support: + Core\" in this package, e.g. \"RequestHeaderModifier\"." + enum: + - RequestHeaderModifier + - RequestRedirect + type: string + required: + - type + type: object + maxItems: 16 + type: array + matches: + default: + - path: + type: PathPrefix + value: / + description: "Matches define conditions used for matching the + rule against incoming HTTP requests. Each match is independent, + i.e. this rule will be matched if **any** one of the matches + is satisfied. \n For example, take the following matches configuration: + \n ``` matches: - path: value: \"/foo\" headers: - + name: \"version\" value: \"v2\" - path: value: \"/v2/foo\" + ``` \n For a request to match against this rule, a request + must satisfy EITHER of the two conditions: \n - path prefixed + with `/foo` AND contains the header `version: v2` - path prefix + of `/v2/foo` \n See the documentation for HTTPRouteMatch on + how to specify multiple match conditions that should be ANDed + together. \n If no matches are specified, the default is a + prefix path match on \"/\", which has the effect of matching + every HTTP request. \n Proxy or Load Balancer routing configuration + generated from HTTPRoutes MUST prioritize rules based on the + following criteria, continuing on ties. Precedence must be + given to the the Rule with the largest number of: \n * Characters + in a matching non-wildcard hostname. * Characters in a matching + hostname. * Characters in a matching path. * Header matches. + * Query param matches. \n If ties still exist across multiple + Routes, matching precedence MUST be determined in order of + the following criteria, continuing on ties: \n * The oldest + Route based on creation timestamp. * The Route appearing first + in alphabetical order by \"{namespace}/{name}\". \n If ties + still exist within the Route that has been given precedence, + matching precedence MUST be granted to the first matching + rule meeting the above criteria. \n When no rules matching + a request have been successfully attached to the parent a + request is coming from, a HTTP 404 status code MUST be returned." + items: + description: "HTTPRouteMatch defines the predicate used to + match requests to a given action. Multiple match types are + ANDed together, i.e. the match will evaluate to true only + if all conditions are satisfied. \n For example, the match + below will match a HTTP request only if its path starts + with `/foo` AND it contains the `version: v1` header: \n + ``` match: path: value: \"/foo\" headers: - name: + \"version\" value \"v1\" ```" + properties: + headers: + description: Headers specifies HTTP request header matchers. + Multiple match values are ANDed together, meaning, a + request must match all the specified headers to select + the route. + items: + description: HTTPHeaderMatch describes how to select + a HTTP route by matching HTTP request headers. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case insensitive. + (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent header + names, only the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST be + ignored. Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered equivalent. + \n When a header is repeated in an HTTP request, + it is implementation-specific behavior as to how + this is represented. Generally, proxies should + follow the guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, with special + handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the header. \n Support: Core (Exact) + \n Support: Custom (RegularExpression) \n Since + RegularExpression HeaderMatchType has custom conformance, + implementations can support POSIX, PCRE or any + other dialects of regular expressions. Please + read the implementation's documentation to determine + the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP Header to + be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. When + specified, this route will be matched only if the request + has the specified method. \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path matcher. + If this field is not specified, a default prefix match + on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match against + the path Value. \n Support: Core (Exact, PathPrefix) + \n Support: Custom (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match against. + maxLength: 1024 + type: string + type: object + queryParams: + description: QueryParams specifies HTTP query parameter + matchers. Multiple match values are ANDed together, + meaning, a request must match all the specified query + parameters to select the route. + items: + description: HTTPQueryParamMatch describes how to select + a HTTP route by matching HTTP query parameters. + properties: + name: + description: Name is the name of the HTTP query + param to be matched. This must be an exact string + match. (See https://tools.ietf.org/html/rfc7230#section-2.7.3). + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the query parameter. \n Support: + Extended (Exact) \n Support: Custom (RegularExpression) + \n Since RegularExpression QueryParamMatchType + has custom conformance, implementations can support + POSIX, PCRE or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP query param + to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + maxItems: 8 + type: array + timeouts: + description: "Timeouts defines the timeouts that can be configured + for an HTTP request. \n Support: Core \n " + properties: + backendRequest: + description: "BackendRequest specifies a timeout for an + individual request from the gateway to a backend service. + Typically used in conjunction with automatic retries, + if supported by an implementation. Default is the value + of Request timeout. \n Support: Extended" + format: duration + type: string + request: + description: "Request specifies a timeout for responding + to client HTTP requests, disabled by default. \n For example, + the following rule will timeout if a client request is + taking longer than 10 seconds to complete: \n ``` rules: + - timeouts: request: 10s backendRefs: ... ``` \n Support: + Core" + format: duration + type: string + type: object + type: object + maxItems: 16 + type: array + type: object + status: + description: Status defines the current state of HTTPRoute. + properties: + parents: + description: "Parents is a list of parent resources (usually Gateways) + that are associated with the route, and the status of the route + with respect to each parent. When this route attaches to a parent, + the controller that manages the parent must add an entry to this + list when the controller first sees the route and should update + the entry as appropriate when the route or gateway is modified. + \n Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this + API can only populate Route status for the Gateways/parent resources + they are responsible for. \n A maximum of 32 Gateways will be represented + in this list. An empty list means the route has not been attached + to any Gateway." + items: + description: RouteParentStatus describes the status of a route with + respect to an associated Parent. + properties: + conditions: + description: "Conditions describes the status of the route with + respect to the Gateway. Note that the route's availability + is also subject to the Gateway's own status conditions and + listener status. \n If the Route's ParentRef specifies an + existing Gateway that supports Routes of this kind AND that + Gateway's controller has sufficient access, then that Gateway's + controller MUST set the \"Accepted\" condition on the Route, + to indicate whether the route has been accepted or rejected + by the Gateway, and why. \n A Route MUST be considered \"Accepted\" + if at least one of the Route's rules is implemented by the + Gateway. \n There are a number of cases where the \"Accepted\" + condition may not be set due to lack of controller visibility, + that includes when: \n * The Route refers to a non-existent + parent. * The Route is of a type that the controller does + not support. * The Route is in a namespace the the controller + does not have access to." + items: + description: "Condition contains details for one aspect of + the current state of this API Resource. --- This struct + is intended for direct use as an array at the field path + .status.conditions. For example, type FooStatus struct{ + \ // Represents the observations of a foo's current state. + \ // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type + \ // +patchStrategy=merge // +listType=map // + +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` + \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should + be when the underlying condition changed. If that is + not known, then using the time when the API field changed + is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, + if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the + current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier + indicating the reason for the condition's last transition. + Producers of specific condition types may define expected + values and meanings for this field, and whether the + values are considered a guaranteed API. The value should + be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across + resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability + to deconflict is important. The regex it matches is + (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: "ControllerName is a domain/path string that indicates + the name of the controller that wrote this status. This corresponds + with the controllerName field on GatewayClass. \n Example: + \"example.net/gateway-controller\". \n The format of this + field is DOMAIN \"/\" PATH, where DOMAIN and PATH are valid + Kubernetes names (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + \n Controllers MUST populate this field when writing status. + Controllers should ensure that entries to status populated + with their ControllerName are cleaned up when they are no + longer necessary." + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: ParentRef corresponds with a ParentRef in the spec + that this RouteParentStatus struct describes the status of. + properties: + group: + default: policy.linkerd.io + description: "Group is the group of the referent. \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: + Core (Gateway) Support: Custom (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. + When unspecified (or empty string), this refers to the + local namespace of the Route. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + sectionName: + description: "SectionName is the name of a section within + the target resource. In the following resources, SectionName + is interpreted as the following: \n * Gateway: Listener + Name. When both Port (experimental) and SectionName are + specified, the name and port of the selected listener + must match both specified values. \n Implementations MAY + choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName + is interpreted. \n When unspecified (empty string), this + will reference the entire resource. For the purpose of + status, an attachment is considered successful if at least + one section in the parent resource accepts it. For example, + Gateway listeners can restrict which Routes can attach + to them by Route kind, namespace, or hostname. If 1 of + 2 Gateway listeners accept attachment from the referencing + Route, the Route MUST be considered successfully attached. + If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/meshtls-authentication.yaml b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/meshtls-authentication.yaml new file mode 100644 index 0000000..6840d5e --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/meshtls-authentication.yaml @@ -0,0 +1,88 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: meshtlsauthentications.policy.linkerd.io + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + linkerd.io/control-plane-ns: {{.Release.Namespace}} +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: MeshTLSAuthentication + plural: meshtlsauthentications + singular: meshtlsauthentication + shortNames: [meshtlsauthn] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + MeshTLSAuthentication defines a list of authenticated client IDs + to be referenced by an `AuthorizationPolicy`. If a client + connection has the mutually-authenticated identity that matches + ANY of the of the provided identities, the connection is + considered authenticated. + type: object + oneOf: + - required: [identities] + - required: [identityRefs] + properties: + identities: + description: >- + Authorizes clients with the provided proxy identity strings + (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + minItems: 1 + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + identityRefs: + type: array + minItems: 1 + items: + type: object + required: + - kind + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. When unspecified, + this refers to all resources of the specified Group + and Kind in the specified namespace. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/network-authentication.yaml b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/network-authentication.yaml new file mode 100644 index 0000000..cef15d3 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/network-authentication.yaml @@ -0,0 +1,53 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: networkauthentications.policy.linkerd.io + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + linkerd.io/control-plane-ns: {{.Release.Namespace}} +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: NetworkAuthentication + plural: networkauthentications + singular: networkauthentication + shortNames: [netauthn, networkauthn] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + NetworkAuthentication defines a list of authenticated client + networks to be referenced by an `AuthorizationPolicy`. If a + client connection originates from ANY of the of the provided + networks, the connection is considered authenticated. + type: object + required: [networks] + properties: + networks: + type: array + items: + type: object + required: [cidr] + properties: + cidr: + description: >- + The CIDR of the network to be authorized. + type: string + except: + description: >- + A list of IP networks/addresses not to be included in + the above `cidr`. + type: array + items: + type: string diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/server-authorization.yaml b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/server-authorization.yaml new file mode 100644 index 0000000..33fb659 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/server-authorization.yaml @@ -0,0 +1,266 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: serverauthorizations.policy.linkerd.io + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + linkerd.io/control-plane-ns: {{.Release.Namespace}} +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: ServerAuthorization + plural: serverauthorizations + singular: serverauthorization + shortNames: [saz, serverauthz, srvauthz] + versions: + - name: v1alpha1 + served: true + storage: false + deprecated: true + deprecationWarning: "policy.linkerd.io/v1alpha1 ServerAuthorization is deprecated; use policy.linkerd.io/v1beta1 ServerAuthorization" + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied servers. + type: object + required: [server, client] + properties: + server: + description: >- + Identifies servers in the same namespace for which this + authorization applies. + + Only one of `name` or `selector` may be specified. + type: object + oneOf: + - required: [name] + - required: [selector] + properties: + name: + description: References a `Server` instance by name + type: string + pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' + selector: + description: >- + A label query over servers on which this authorization applies. + type: object + properties: + matchLabels: + type: object + x-kubernetes-preserve-unknown-fields: true + matchExpressions: + type: array + items: + type: object + required: [key, operator] + properties: + key: + type: string + operator: + type: string + enum: [In, NotIn, Exists, DoesNotExist] + values: + type: array + items: + type: string + client: + description: Describes clients authorized to access a server. + type: object + properties: + networks: + description: >- + Limits the client IP addresses to which this + authorization applies. If unset, the server chooses a + default (typically, all IPs or the cluster's pod + network). + type: array + items: + type: object + required: [cidr] + properties: + cidr: + type: string + except: + type: array + items: + type: string + unauthenticated: + description: >- + Authorizes unauthenticated clients to access a server. + type: boolean + meshTLS: + type: object + properties: + unauthenticatedTLS: + type: boolean + description: >- + Indicates that no client identity is required for + communication. + + This is mostly important for the identity + controller, which must terminate TLS connections + from clients that do not yet have a certificate. + identities: + description: >- + Authorizes clients with the provided proxy identity + strings (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + serviceAccounts: + description: >- + Authorizes clients with the provided proxy identity + service accounts (as provided via MTLS) + type: array + items: + type: object + required: [name] + properties: + name: + description: The ServiceAccount's name. + type: string + pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' + namespace: + description: >- + The ServiceAccount's namespace. If unset, the + authorization's namespace is used. + type: string + pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' + - name: v1beta1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied servers. + type: object + required: [server, client] + properties: + server: + description: >- + Identifies servers in the same namespace for which this + authorization applies. + + Only one of `name` or `selector` may be specified. + type: object + oneOf: + - required: [name] + - required: [selector] + properties: + name: + description: References a `Server` instance by name + type: string + pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' + selector: + description: >- + A label query over servers on which this authorization applies. + type: object + properties: + matchLabels: + type: object + x-kubernetes-preserve-unknown-fields: true + matchExpressions: + type: array + items: + type: object + required: [key, operator] + properties: + key: + type: string + operator: + type: string + enum: [In, NotIn, Exists, DoesNotExist] + values: + type: array + items: + type: string + client: + description: Describes clients authorized to access a server. + type: object + properties: + networks: + description: >- + Limits the client IP addresses to which this + authorization applies. If unset, the server chooses a + default (typically, all IPs or the cluster's pod + network). + type: array + items: + type: object + required: [cidr] + properties: + cidr: + type: string + except: + type: array + items: + type: string + unauthenticated: + description: >- + Authorizes unauthenticated clients to access a server. + type: boolean + meshTLS: + type: object + properties: + unauthenticatedTLS: + type: boolean + description: >- + Indicates that no client identity is required for + communication. + + This is mostly important for the identity + controller, which must terminate TLS connections + from clients that do not yet have a certificate. + identities: + description: >- + Authorizes clients with the provided proxy identity + strings (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + serviceAccounts: + description: >- + Authorizes clients with the provided proxy identity + service accounts (as provided via MTLS) + type: array + items: + type: object + required: [name] + properties: + name: + description: The ServiceAccount's name. + type: string + pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' + namespace: + description: >- + The ServiceAccount's namespace. If unset, the + authorization's namespace is used. + type: string + pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' + additionalPrinterColumns: + - name: Server + type: string + description: The server that this grants access to + jsonPath: .spec.server.name diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/server.yaml b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/server.yaml new file mode 100644 index 0000000..a5d9f26 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/policy/server.yaml @@ -0,0 +1,132 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: servers.policy.linkerd.io + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + linkerd.io/control-plane-ns: {{.Release.Namespace}} +spec: + group: policy.linkerd.io + names: + kind: Server + plural: servers + singular: server + shortNames: [srv] + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: false + deprecated: true + deprecationWarning: "policy.linkerd.io/v1alpha1 Server is deprecated; use policy.linkerd.io/v1beta1 Server" + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + type: object + required: + - podSelector + - port + properties: + podSelector: + type: object + description: >- + Selects pods in the same namespace. + oneOf: + - required: [matchExpressions] + - required: [matchLabels] + properties: + matchLabels: + type: object + x-kubernetes-preserve-unknown-fields: true + matchExpressions: + type: array + items: + type: object + required: [key, operator] + properties: + key: + type: string + operator: + type: string + enum: [In, NotIn, Exists, DoesNotExist] + values: + type: array + items: + type: string + port: + description: >- + A port name or number. Must exist in a pod spec. + x-kubernetes-int-or-string: true + proxyProtocol: + description: >- + Configures protocol discovery for inbound connections. + + Supersedes the `config.linkerd.io/opaque-ports` annotation. + type: string + default: unknown + - name: v1beta1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + type: object + required: + - podSelector + - port + properties: + podSelector: + type: object + description: >- + Selects pods in the same namespace. + + The result of matchLabels and matchExpressions are ANDed. + Selects all if empty. + properties: + matchLabels: + type: object + x-kubernetes-preserve-unknown-fields: true + matchExpressions: + type: array + items: + type: object + required: [key, operator] + properties: + key: + type: string + operator: + type: string + enum: [In, NotIn, Exists, DoesNotExist] + values: + type: array + items: + type: string + port: + description: >- + A port name or number. Must exist in a pod spec. + x-kubernetes-int-or-string: true + proxyProtocol: + description: >- + Configures protocol discovery for inbound connections. + + Supersedes the `config.linkerd.io/opaque-ports` annotation. + type: string + default: unknown + additionalPrinterColumns: + - name: Port + type: string + description: The port the server is listening on + jsonPath: .spec.port + - name: Protocol + type: string + description: The protocol of the server + jsonPath: .spec.proxyProtocol diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/serviceprofile.yaml b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/serviceprofile.yaml new file mode 100644 index 0000000..d1a0bfc --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/templates/serviceprofile.yaml @@ -0,0 +1,278 @@ +--- +### +### Service Profile CRD +### +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: serviceprofiles.linkerd.io + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + linkerd.io/control-plane-ns: {{.Release.Namespace}} +spec: + group: linkerd.io + versions: + - name: v1alpha1 + served: true + storage: false + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + description: Spec is the custom resource spec + required: + - routes + properties: + dstOverrides: + type: array + required: + - authority + - weight + items: + type: object + description: WeightedDst is a weighted alternate destination. + properties: + authority: + type: string + weight: + x-kubernetes-int-or-string: true + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + opaquePorts: + type: array + items: + type: string + retryBudget: + type: object + required: + - minRetriesPerSecond + - retryRatio + - ttl + description: RetryBudget describes the maximum number of retries that should be issued to this service. + properties: + minRetriesPerSecond: + format: int32 + type: integer + retryRatio: + type: number + format: float + ttl: + type: string + routes: + type: array + items: + type: object + description: RouteSpec specifies a Route resource. + required: + - condition + - name + properties: + condition: + type: object + description: RequestMatch describes the conditions under which to match a Route. + properties: + pathRegex: + type: string + method: + type: string + all: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + any: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + not: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + isRetryable: + type: boolean + name: + type: string + timeout: + type: string + responseClasses: + type: array + items: + type: object + required: + - condition + description: ResponseClass describes how to classify a response (e.g. success or failures). + properties: + condition: + type: object + description: ResponseMatch describes the conditions under + which to classify a response. + properties: + all: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + any: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + not: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + status: + type: object + description: Range describes a range of integers (e.g. status codes). + properties: + max: + format: int32 + type: integer + min: + format: int32 + type: integer + isFailure: + type: boolean + - name: v1alpha2 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + description: Spec is the custom resource spec + properties: + dstOverrides: + type: array + required: + - authority + - weight + items: + type: object + description: WeightedDst is a weighted alternate destination. + properties: + authority: + type: string + weight: + x-kubernetes-int-or-string: true + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + opaquePorts: + type: array + items: + type: string + retryBudget: + type: object + required: + - minRetriesPerSecond + - retryRatio + - ttl + description: RetryBudget describes the maximum number of retries that should be issued to this service. + properties: + minRetriesPerSecond: + format: int32 + type: integer + retryRatio: + type: number + format: float + ttl: + type: string + routes: + type: array + items: + type: object + description: RouteSpec specifies a Route resource. + required: + - condition + - name + properties: + condition: + type: object + description: RequestMatch describes the conditions under which to match a Route. + properties: + pathRegex: + type: string + method: + type: string + all: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + any: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + not: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + isRetryable: + type: boolean + name: + type: string + timeout: + type: string + responseClasses: + type: array + items: + type: object + required: + - condition + description: ResponseClass describes how to classify a response (e.g. success or failures). + properties: + condition: + type: object + description: ResponseMatch describes the conditions under + which to classify a response. + properties: + all: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + any: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + not: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + status: + type: object + description: Range describes a range of integers (e.g. status codes). + properties: + max: + format: int32 + type: integer + min: + format: int32 + type: integer + isFailure: + type: boolean + scope: Namespaced + preserveUnknownFields: false + names: + plural: serviceprofiles + singular: serviceprofile + kind: ServiceProfile + shortNames: + - sp diff --git a/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/values.yaml b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/values.yaml new file mode 100644 index 0000000..3621451 --- /dev/null +++ b/linkerd/charts/linkerd-crds-1.8.0/linkerd-crds/values.yaml @@ -0,0 +1 @@ +enableHttpRoutes: true diff --git a/linkerd/charts/linkerd-crds/.helmignore b/linkerd/charts/linkerd-crds/.helmignore new file mode 100644 index 0000000..79c90a8 --- /dev/null +++ b/linkerd/charts/linkerd-crds/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +OWNERS +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/linkerd/charts/linkerd-crds/Chart.lock b/linkerd/charts/linkerd-crds/Chart.lock new file mode 100644 index 0000000..a62a030 --- /dev/null +++ b/linkerd/charts/linkerd-crds/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: partials + repository: file://../partials + version: 0.1.0 +digest: sha256:8e42f9c9d4a2dc883f17f94d6044c97518ced19ad0922f47b8760e47135369ba +generated: "2021-08-17T10:42:52.610449255-05:00" diff --git a/linkerd/charts/linkerd-crds/Chart.yaml b/linkerd/charts/linkerd-crds/Chart.yaml new file mode 100644 index 0000000..e368bc6 --- /dev/null +++ b/linkerd/charts/linkerd-crds/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +dependencies: +- name: partials + repository: file://../partials + version: 0.1.0 +description: 'Linkerd gives you observability, reliability, and security for your + microservices — with no code change required. ' +home: https://linkerd.io +icon: https://linkerd.io/images/logo-only-200h.png +keywords: +- service-mesh +kubeVersion: '>=1.21.0-0' +maintainers: +- email: cncf-linkerd-dev@lists.cncf.io + name: Linkerd authors + url: https://linkerd.io/ +name: linkerd-crds +sources: +- https://github.com/linkerd/linkerd2/ +type: application +version: 1.8.0 diff --git a/linkerd/charts/linkerd-crds/README.md b/linkerd/charts/linkerd-crds/README.md new file mode 100644 index 0000000..28cf91e --- /dev/null +++ b/linkerd/charts/linkerd-crds/README.md @@ -0,0 +1,76 @@ +# linkerd-crds + +Linkerd gives you observability, reliability, and security +for your microservices — with no code change required. + +![Version: 1.8.0](https://img.shields.io/badge/Version-1.8.0-informational?style=flat-square) +![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) + +**Homepage:** + +## Quickstart and documentation + +You can run Linkerd on any Kubernetes 1.21+ cluster in a matter of seconds. See +the [Linkerd Getting Started Guide][getting-started] for how. + +For more comprehensive documentation, start with the [Linkerd +docs][linkerd-docs]. + +## Adding Linkerd's Helm repository + +```bash +# To add the repo for Linkerd stable releases: +helm repo add linkerd https://helm.linkerd.io/stable +# To add the repo for Linkerd edge releases: +helm repo add linkerd-edge https://helm.linkerd.io/edge +``` + +The following instructions use the `linkerd` repo. For installing an edge +release, just replace with `linkerd-edge`. + +## Installing the linkerd-crds chart + +This installs the `linkerd-crds` chart, which only persists the CRDs that +Linkerd requires. + +After installing this chart, you need then to install the +`linkerd-control-plane` chart in the same namespace, which provides all the +linkerd core control components. + +```bash +helm install linkerd-crds -n linkerd --create-namespace linkerd/linkerd-crds +``` + +## Get involved + +* Check out Linkerd's source code at [GitHub][linkerd2]. +* Join Linkerd's [user mailing list][linkerd-users], [developer mailing + list][linkerd-dev], and [announcements mailing list][linkerd-announce]. +* Follow [@linkerd][twitter] on Twitter. +* Join the [Linkerd Slack][slack]. + +[getting-started]: https://linkerd.io/2/getting-started/ +[linkerd2]: https://github.com/linkerd/linkerd2 +[linkerd-announce]: https://lists.cncf.io/g/cncf-linkerd-announce +[linkerd-dev]: https://lists.cncf.io/g/cncf-linkerd-dev +[linkerd-docs]: https://linkerd.io/2/overview/ +[linkerd-users]: https://lists.cncf.io/g/cncf-linkerd-users +[slack]: http://slack.linkerd.io +[twitter]: https://twitter.com/linkerd + +## Requirements + +Kubernetes: `>=1.21.0-0` + +| Repository | Name | Version | +|------------|------|---------| +| file://../partials | partials | 0.1.0 | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| enableHttpRoutes | bool | `true` | | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/linkerd/charts/linkerd-crds/README.md.gotmpl b/linkerd/charts/linkerd-crds/README.md.gotmpl new file mode 100644 index 0000000..b396a6f --- /dev/null +++ b/linkerd/charts/linkerd-crds/README.md.gotmpl @@ -0,0 +1,64 @@ +{{ template "chart.header" . }} +{{ template "chart.description" . }} + +{{ template "chart.versionBadge" . }} +{{ template "chart.typeBadge" . }} +{{ template "chart.appVersionBadge" . }} + +{{ template "chart.homepageLine" . }} + +## Quickstart and documentation + +You can run Linkerd on any Kubernetes 1.21+ cluster in a matter of seconds. See +the [Linkerd Getting Started Guide][getting-started] for how. + +For more comprehensive documentation, start with the [Linkerd +docs][linkerd-docs]. + +## Adding Linkerd's Helm repository + +```bash +# To add the repo for Linkerd stable releases: +helm repo add linkerd https://helm.linkerd.io/stable +# To add the repo for Linkerd edge releases: +helm repo add linkerd-edge https://helm.linkerd.io/edge +``` + +The following instructions use the `linkerd` repo. For installing an edge +release, just replace with `linkerd-edge`. + +## Installing the linkerd-crds chart + +This installs the `linkerd-crds` chart, which only persists the CRDs that +Linkerd requires. + +After installing this chart, you need then to install the +`linkerd-control-plane` chart in the same namespace, which provides all the +linkerd core control components. + +```bash +helm install linkerd-crds -n linkerd --create-namespace linkerd/linkerd-crds +``` + +## Get involved + +* Check out Linkerd's source code at [GitHub][linkerd2]. +* Join Linkerd's [user mailing list][linkerd-users], [developer mailing + list][linkerd-dev], and [announcements mailing list][linkerd-announce]. +* Follow [@linkerd][twitter] on Twitter. +* Join the [Linkerd Slack][slack]. + +[getting-started]: https://linkerd.io/2/getting-started/ +[linkerd2]: https://github.com/linkerd/linkerd2 +[linkerd-announce]: https://lists.cncf.io/g/cncf-linkerd-announce +[linkerd-dev]: https://lists.cncf.io/g/cncf-linkerd-dev +[linkerd-docs]: https://linkerd.io/2/overview/ +[linkerd-users]: https://lists.cncf.io/g/cncf-linkerd-users +[slack]: http://slack.linkerd.io +[twitter]: https://twitter.com/linkerd + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/linkerd/charts/linkerd-crds/charts/partials/.helmignore b/linkerd/charts/linkerd-crds/charts/partials/.helmignore new file mode 100644 index 0000000..f0c1319 --- /dev/null +++ b/linkerd/charts/linkerd-crds/charts/partials/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/linkerd/charts/linkerd-crds/charts/partials/Chart.yaml b/linkerd/charts/linkerd-crds/charts/partials/Chart.yaml new file mode 100644 index 0000000..23cfc16 --- /dev/null +++ b/linkerd/charts/linkerd-crds/charts/partials/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +description: 'A Helm chart containing Linkerd partial templates, depended by the ''linkerd'' + and ''patch'' charts. ' +name: partials +version: 0.1.0 diff --git a/linkerd/charts/linkerd-crds/charts/partials/README.md b/linkerd/charts/linkerd-crds/charts/partials/README.md new file mode 100644 index 0000000..e5fbd39 --- /dev/null +++ b/linkerd/charts/linkerd-crds/charts/partials/README.md @@ -0,0 +1,9 @@ +# partials + +A Helm chart containing Linkerd partial templates, +depended by the 'linkerd' and 'patch' charts. + +![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/linkerd/charts/linkerd-crds/charts/partials/README.md.gotmpl b/linkerd/charts/linkerd-crds/charts/partials/README.md.gotmpl new file mode 100644 index 0000000..37f5101 --- /dev/null +++ b/linkerd/charts/linkerd-crds/charts/partials/README.md.gotmpl @@ -0,0 +1,14 @@ +{{ template "chart.header" . }} +{{ template "chart.description" . }} + +{{ template "chart.versionBadge" . }} +{{ template "chart.typeBadge" . }} +{{ template "chart.appVersionBadge" . }} + +{{ template "chart.homepageLine" . }} + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/linkerd/charts/linkerd-crds/charts/partials/templates/NOTES.txt b/linkerd/charts/linkerd-crds/charts/partials/templates/NOTES.txt new file mode 100644 index 0000000..e69de29 diff --git a/linkerd/charts/linkerd-crds/charts/partials/templates/_affinity.tpl b/linkerd/charts/linkerd-crds/charts/partials/templates/_affinity.tpl new file mode 100644 index 0000000..5dde1da --- /dev/null +++ b/linkerd/charts/linkerd-crds/charts/partials/templates/_affinity.tpl @@ -0,0 +1,38 @@ +{{ define "linkerd.pod-affinity" -}} +podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: {{ default "linkerd.io/control-plane-component" .label }} + operator: In + values: + - {{ .component }} + topologyKey: topology.kubernetes.io/zone + weight: 100 + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: {{ default "linkerd.io/control-plane-component" .label }} + operator: In + values: + - {{ .component }} + topologyKey: kubernetes.io/hostname +{{- end }} + +{{ define "linkerd.node-affinity" -}} +nodeAffinity: +{{- toYaml .Values.nodeAffinity | trim | nindent 2 }} +{{- end }} + +{{ define "linkerd.affinity" -}} +{{- if or .Values.enablePodAntiAffinity .Values.nodeAffinity -}} +affinity: +{{- end }} +{{- if .Values.enablePodAntiAffinity -}} +{{- include "linkerd.pod-affinity" . | nindent 2 }} +{{- end }} +{{- if .Values.nodeAffinity -}} +{{- include "linkerd.node-affinity" . | nindent 2 }} +{{- end }} +{{- end }} diff --git a/linkerd/charts/linkerd-crds/charts/partials/templates/_capabilities.tpl b/linkerd/charts/linkerd-crds/charts/partials/templates/_capabilities.tpl new file mode 100644 index 0000000..a595d74 --- /dev/null +++ b/linkerd/charts/linkerd-crds/charts/partials/templates/_capabilities.tpl @@ -0,0 +1,16 @@ +{{- define "partials.proxy.capabilities" -}} +capabilities: + {{- if .Values.proxy.capabilities.add }} + add: + {{- toYaml .Values.proxy.capabilities.add | trim | nindent 4 }} + {{- end }} + {{- if .Values.proxy.capabilities.drop }} + drop: + {{- toYaml .Values.proxy.capabilities.drop | trim | nindent 4 }} + {{- end }} +{{- end -}} + +{{- define "partials.proxy-init.capabilities.drop" -}} +drop: +{{ toYaml .Values.proxyInit.capabilities.drop | trim }} +{{- end -}} diff --git a/linkerd/charts/linkerd-crds/charts/partials/templates/_debug.tpl b/linkerd/charts/linkerd-crds/charts/partials/templates/_debug.tpl new file mode 100644 index 0000000..cf0eb14 --- /dev/null +++ b/linkerd/charts/linkerd-crds/charts/partials/templates/_debug.tpl @@ -0,0 +1,6 @@ +{{- define "partials.debug" -}} +image: {{.Values.debugContainer.image.name}}:{{.Values.debugContainer.image.version | default .Values.linkerdVersion}} +imagePullPolicy: {{.Values.debugContainer.image.pullPolicy | default .Values.imagePullPolicy}} +name: linkerd-debug +terminationMessagePolicy: FallbackToLogsOnError +{{- end -}} diff --git a/linkerd/charts/linkerd-crds/charts/partials/templates/_helpers.tpl b/linkerd/charts/linkerd-crds/charts/partials/templates/_helpers.tpl new file mode 100644 index 0000000..b6cdc34 --- /dev/null +++ b/linkerd/charts/linkerd-crds/charts/partials/templates/_helpers.tpl @@ -0,0 +1,14 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Splits a coma separated list into a list of string values. +For example "11,22,55,44" will become "11","22","55","44" +*/}} +{{- define "partials.splitStringList" -}} +{{- if gt (len (toString .)) 0 -}} +{{- $ports := toString . | splitList "," -}} +{{- $last := sub (len $ports) 1 -}} +{{- range $i,$port := $ports -}} +"{{$port}}"{{ternary "," "" (ne $i $last)}} +{{- end -}} +{{- end -}} +{{- end -}} diff --git a/linkerd/charts/linkerd-crds/charts/partials/templates/_metadata.tpl b/linkerd/charts/linkerd-crds/charts/partials/templates/_metadata.tpl new file mode 100644 index 0000000..04d2f1b --- /dev/null +++ b/linkerd/charts/linkerd-crds/charts/partials/templates/_metadata.tpl @@ -0,0 +1,17 @@ +{{- define "partials.annotations.created-by" -}} +linkerd.io/created-by: {{ .Values.cliVersion | default (printf "linkerd/helm %s" ( (.Values.image).version | default .Values.linkerdVersion)) }} +{{- end -}} + +{{- define "partials.proxy.annotations" -}} +linkerd.io/proxy-version: {{.Values.proxy.image.version | default .Values.linkerdVersion}} +cluster-autoscaler.kubernetes.io/safe-to-evict: "true" +linkerd.io/trust-root-sha256: {{ .Values.identityTrustAnchorsPEM | sha256sum }} +{{- end -}} + +{{/* +To add labels to the control-plane components, instead update at individual component manifests as +adding here would also update `spec.selector.matchLabels` which are immutable and would fail upgrades. +*/}} +{{- define "partials.proxy.labels" -}} +linkerd.io/proxy-{{.workloadKind}}: {{.component}} +{{- end -}} diff --git a/linkerd/charts/linkerd-crds/charts/partials/templates/_network-validator.tpl b/linkerd/charts/linkerd-crds/charts/partials/templates/_network-validator.tpl new file mode 100644 index 0000000..58f36e6 --- /dev/null +++ b/linkerd/charts/linkerd-crds/charts/partials/templates/_network-validator.tpl @@ -0,0 +1,32 @@ +{{- define "partials.network-validator" -}} +name: linkerd-network-validator +image: {{.Values.proxy.image.name}}:{{.Values.proxy.image.version | default .Values.linkerdVersion }} +imagePullPolicy: {{.Values.proxy.image.pullPolicy | default .Values.imagePullPolicy}} +{{ include "partials.resources" .Values.proxyInit.resources }} +{{- if or .Values.networkValidator.enableSecurityContext }} +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 65534 + seccompProfile: + type: RuntimeDefault +{{- end }} +command: + - /usr/lib/linkerd/linkerd2-network-validator +args: + - --log-format + - {{ .Values.networkValidator.logFormat }} + - --log-level + - {{ .Values.networkValidator.logLevel }} + - --connect-addr + - {{ .Values.networkValidator.connectAddr }} + - --listen-addr + - {{ .Values.networkValidator.listenAddr }} + - --timeout + - {{ .Values.networkValidator.timeout }} + +{{- end -}} diff --git a/linkerd/charts/linkerd-crds/charts/partials/templates/_nodeselector.tpl b/linkerd/charts/linkerd-crds/charts/partials/templates/_nodeselector.tpl new file mode 100644 index 0000000..4cde0ab --- /dev/null +++ b/linkerd/charts/linkerd-crds/charts/partials/templates/_nodeselector.tpl @@ -0,0 +1,4 @@ +{{- define "linkerd.node-selector" -}} +nodeSelector: +{{- toYaml .Values.nodeSelector | trim | nindent 2 }} +{{- end -}} diff --git a/linkerd/charts/linkerd-crds/charts/partials/templates/_proxy-config-ann.tpl b/linkerd/charts/linkerd-crds/charts/partials/templates/_proxy-config-ann.tpl new file mode 100644 index 0000000..9651b3b --- /dev/null +++ b/linkerd/charts/linkerd-crds/charts/partials/templates/_proxy-config-ann.tpl @@ -0,0 +1,18 @@ +{{- define "partials.proxy.config.annotations" -}} +{{- with .cpu }} +{{- with .request -}} +config.linkerd.io/proxy-cpu-request: {{. | quote}} +{{end}} +{{- with .limit -}} +config.linkerd.io/proxy-cpu-limit: {{. | quote}} +{{- end}} +{{- end}} +{{- with .memory }} +{{- with .request }} +config.linkerd.io/proxy-memory-request: {{. | quote}} +{{end}} +{{- with .limit -}} +config.linkerd.io/proxy-memory-limit: {{. | quote}} +{{- end}} +{{- end }} +{{- end }} diff --git a/linkerd/charts/linkerd-crds/charts/partials/templates/_proxy-init.tpl b/linkerd/charts/linkerd-crds/charts/partials/templates/_proxy-init.tpl new file mode 100644 index 0000000..91cc96e --- /dev/null +++ b/linkerd/charts/linkerd-crds/charts/partials/templates/_proxy-init.tpl @@ -0,0 +1,89 @@ +{{- define "partials.proxy-init" -}} +args: +{{- if (.Values.proxyInit.iptablesMode | default "legacy" | eq "nft") }} +- --firewall-bin-path +- "iptables-nft" +- --firewall-save-bin-path +- "iptables-nft-save" +{{- else if not (eq .Values.proxyInit.iptablesMode "legacy") }} +{{ fail (printf "Unsupported value \"%s\" for proxyInit.iptablesMode\nValid values: [\"nft\", \"legacy\"]" .Values.proxyInit.iptablesMode) }} +{{- end }} +- --incoming-proxy-port +- {{.Values.proxy.ports.inbound | quote}} +- --outgoing-proxy-port +- {{.Values.proxy.ports.outbound | quote}} +- --proxy-uid +- {{.Values.proxy.uid | quote}} +- --inbound-ports-to-ignore +- "{{.Values.proxy.ports.control}},{{.Values.proxy.ports.admin}}{{ternary (printf ",%s" (.Values.proxyInit.ignoreInboundPorts | toString)) "" (not (empty .Values.proxyInit.ignoreInboundPorts)) }}" +{{- if .Values.proxyInit.ignoreOutboundPorts }} +- --outbound-ports-to-ignore +- {{.Values.proxyInit.ignoreOutboundPorts | quote}} +{{- end }} +{{- if .Values.proxyInit.closeWaitTimeoutSecs }} +- --timeout-close-wait-secs +- {{ .Values.proxyInit.closeWaitTimeoutSecs | quote}} +{{- end }} +{{- if .Values.proxyInit.logFormat }} +- --log-format +- {{ .Values.proxyInit.logFormat }} +{{- end }} +{{- if .Values.proxyInit.logLevel }} +- --log-level +- {{ .Values.proxyInit.logLevel }} +{{- end }} +{{- if .Values.proxyInit.skipSubnets }} +- --subnets-to-ignore +- {{ .Values.proxyInit.skipSubnets | quote }} +{{- end }} +image: {{.Values.proxyInit.image.name}}:{{.Values.proxyInit.image.version}} +imagePullPolicy: {{.Values.proxyInit.image.pullPolicy | default .Values.imagePullPolicy}} +name: linkerd-init +{{ include "partials.resources" .Values.proxyInit.resources }} +securityContext: + {{- if or .Values.proxyInit.closeWaitTimeoutSecs .Values.proxyInit.privileged }} + allowPrivilegeEscalation: true + {{- else }} + allowPrivilegeEscalation: false + {{- end }} + capabilities: + add: + - NET_ADMIN + - NET_RAW + {{- if .Values.proxyInit.capabilities -}} + {{- if .Values.proxyInit.capabilities.add }} + {{- toYaml .Values.proxyInit.capabilities.add | trim | nindent 4 }} + {{- end }} + {{- if .Values.proxyInit.capabilities.drop -}} + {{- include "partials.proxy-init.capabilities.drop" . | nindent 4 -}} + {{- end }} + {{- end }} + {{- if or .Values.proxyInit.closeWaitTimeoutSecs .Values.proxyInit.privileged }} + privileged: true + {{- else }} + privileged: false + {{- end }} + {{- if .Values.proxyInit.runAsRoot }} + runAsNonRoot: false + runAsUser: 0 + {{- else }} + runAsNonRoot: true + runAsUser: {{ .Values.proxyInit.runAsUser | int | eq 0 | ternary 65534 .Values.proxyInit.runAsUser }} + {{- end }} + readOnlyRootFilesystem: true + seccompProfile: + type: RuntimeDefault +terminationMessagePolicy: FallbackToLogsOnError +{{- if or (not .Values.cniEnabled) .Values.proxyInit.saMountPath }} +volumeMounts: +{{- end -}} +{{- if not .Values.cniEnabled }} +- mountPath: {{.Values.proxyInit.xtMountPath.mountPath}} + name: {{.Values.proxyInit.xtMountPath.name}} +{{- end -}} +{{- if .Values.proxyInit.saMountPath }} +- mountPath: {{.Values.proxyInit.saMountPath.mountPath}} + name: {{.Values.proxyInit.saMountPath.name}} + readOnly: {{.Values.proxyInit.saMountPath.readOnly}} +{{- end -}} +{{- end -}} diff --git a/linkerd/charts/linkerd-crds/charts/partials/templates/_proxy.tpl b/linkerd/charts/linkerd-crds/charts/partials/templates/_proxy.tpl new file mode 100644 index 0000000..f5dd4c2 --- /dev/null +++ b/linkerd/charts/linkerd-crds/charts/partials/templates/_proxy.tpl @@ -0,0 +1,215 @@ +{{ define "partials.proxy" -}} +{{- $trustDomain := (.Values.identityTrustDomain | default .Values.clusterDomain) -}} +env: +- name: _pod_name + valueFrom: + fieldRef: + fieldPath: metadata.name +- name: _pod_ns + valueFrom: + fieldRef: + fieldPath: metadata.namespace +- name: _pod_nodeName + valueFrom: + fieldRef: + fieldPath: spec.nodeName +{{- if .Values.proxy.cores }} +- name: LINKERD2_PROXY_CORES + value: {{.Values.proxy.cores | quote}} +{{- end }} +{{ if .Values.proxy.requireIdentityOnInboundPorts -}} +- name: LINKERD2_PROXY_INBOUND_PORTS_REQUIRE_IDENTITY + value: {{.Values.proxy.requireIdentityOnInboundPorts | quote}} +{{ end -}} +{{ if .Values.proxy.requireTLSOnInboundPorts -}} +- name: LINKERD2_PROXY_INBOUND_PORTS_REQUIRE_TLS + value: {{.Values.proxy.requireTLSOnInboundPorts | quote}} +{{ end -}} +- name: LINKERD2_PROXY_LOG + value: {{.Values.proxy.logLevel | quote}} +- name: LINKERD2_PROXY_LOG_FORMAT + value: {{.Values.proxy.logFormat | quote}} +- name: LINKERD2_PROXY_DESTINATION_SVC_ADDR + value: {{ternary "localhost.:8086" (printf "linkerd-dst-headless.%s.svc.%s.:8086" .Release.Namespace .Values.clusterDomain) (eq (toString .Values.proxy.component) "linkerd-destination")}} +- name: LINKERD2_PROXY_DESTINATION_PROFILE_NETWORKS + value: {{.Values.clusterNetworks | quote}} +- name: LINKERD2_PROXY_POLICY_SVC_ADDR + value: {{ternary "localhost.:8090" (printf "linkerd-policy.%s.svc.%s.:8090" .Release.Namespace .Values.clusterDomain) (eq (toString .Values.proxy.component) "linkerd-destination")}} +- name: LINKERD2_PROXY_POLICY_WORKLOAD + value: "$(_pod_ns):$(_pod_name)" +- name: LINKERD2_PROXY_INBOUND_DEFAULT_POLICY + value: {{.Values.proxy.defaultInboundPolicy}} +- name: LINKERD2_PROXY_POLICY_CLUSTER_NETWORKS + value: {{.Values.clusterNetworks | quote}} +{{ if .Values.proxy.inboundConnectTimeout -}} +- name: LINKERD2_PROXY_INBOUND_CONNECT_TIMEOUT + value: {{.Values.proxy.inboundConnectTimeout | quote}} +{{ end -}} +{{ if .Values.proxy.outboundConnectTimeout -}} +- name: LINKERD2_PROXY_OUTBOUND_CONNECT_TIMEOUT + value: {{.Values.proxy.outboundConnectTimeout | quote}} +{{ end -}} +{{ if .Values.proxy.outboundDiscoveryCacheUnusedTimeout -}} +- name: LINKERD2_PROXY_OUTBOUND_DISCOVERY_IDLE_TIMEOUT + value: {{.Values.proxy.outboundDiscoveryCacheUnusedTimeout | quote}} +{{ end -}} +{{ if .Values.proxy.inboundDiscoveryCacheUnusedTimeout -}} +- name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT + value: {{.Values.proxy.inboundDiscoveryCacheUnusedTimeout | quote}} +{{ end -}} +{{ if .Values.proxy.disableOutboundProtocolDetectTimeout -}} +- name: LINKERD2_PROXY_OUTBOUND_DETECT_TIMEOUT + value: "365d" +{{ end -}} +{{ if .Values.proxy.disableInboundProtocolDetectTimeout -}} +- name: LINKERD2_PROXY_INBOUND_DETECT_TIMEOUT + value: "365d" +{{ end -}} +- name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR + value: 0.0.0.0:{{.Values.proxy.ports.control}} +- name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR + value: 0.0.0.0:{{.Values.proxy.ports.admin}} +- name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR + value: 127.0.0.1:{{.Values.proxy.ports.outbound}} +- name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR + value: 0.0.0.0:{{.Values.proxy.ports.inbound}} +- name: LINKERD2_PROXY_INBOUND_IPS + valueFrom: + fieldRef: + fieldPath: status.podIPs +- name: LINKERD2_PROXY_INBOUND_PORTS + value: {{ .Values.proxy.podInboundPorts | quote }} +{{ if .Values.proxy.isGateway -}} +- name: LINKERD2_PROXY_INBOUND_GATEWAY_SUFFIXES + value: {{printf "svc.%s." .Values.clusterDomain}} +{{ end -}} +{{ if .Values.proxy.isIngress -}} +- name: LINKERD2_PROXY_INGRESS_MODE + value: "true" +{{ end -}} +- name: LINKERD2_PROXY_DESTINATION_PROFILE_SUFFIXES + {{- $internalDomain := printf "svc.%s." .Values.clusterDomain }} + value: {{ternary "." $internalDomain .Values.proxy.enableExternalProfiles}} +- name: LINKERD2_PROXY_INBOUND_ACCEPT_KEEPALIVE + value: 10000ms +- name: LINKERD2_PROXY_OUTBOUND_CONNECT_KEEPALIVE + value: 10000ms +{{ if .Values.proxy.opaquePorts -}} +- name: LINKERD2_PROXY_INBOUND_PORTS_DISABLE_PROTOCOL_DETECTION + value: {{.Values.proxy.opaquePorts | quote}} +{{ end -}} +- name: LINKERD2_PROXY_DESTINATION_CONTEXT + value: | + {"ns":"$(_pod_ns)", "nodeName":"$(_pod_nodeName)", "pod":"$(_pod_name)"} +- name: _pod_sa + valueFrom: + fieldRef: + fieldPath: spec.serviceAccountName +- name: _l5d_ns + value: {{.Release.Namespace}} +- name: _l5d_trustdomain + value: {{$trustDomain}} +- name: LINKERD2_PROXY_IDENTITY_DIR + value: /var/run/linkerd/identity/end-entity +- name: LINKERD2_PROXY_IDENTITY_TRUST_ANCHORS +{{- /* +Pods in the `linkerd` namespace are not injected by the proxy injector and instead obtain +the trust anchor bundle from the `linkerd-identity-trust-roots` configmap. This should not +be used in other contexts. +*/}} +{{- if .Values.proxy.loadTrustBundleFromConfigMap }} + valueFrom: + configMapKeyRef: + name: linkerd-identity-trust-roots + key: ca-bundle.crt +{{ else }} + value: | + {{- required "Please provide the identity trust anchors" .Values.identityTrustAnchorsPEM | trim | nindent 4 }} +{{ end -}} +- name: LINKERD2_PROXY_IDENTITY_TOKEN_FILE +{{- if .Values.identity.serviceAccountTokenProjection }} + value: /var/run/secrets/tokens/linkerd-identity-token +{{ else }} + value: /var/run/secrets/kubernetes.io/serviceaccount/token +{{ end -}} +- name: LINKERD2_PROXY_IDENTITY_SVC_ADDR + value: {{ternary "localhost.:8080" (printf "linkerd-identity-headless.%s.svc.%s.:8080" .Release.Namespace .Values.clusterDomain) (eq (toString .Values.proxy.component) "linkerd-identity")}} +- name: LINKERD2_PROXY_IDENTITY_LOCAL_NAME + value: $(_pod_sa).$(_pod_ns).serviceaccount.identity.{{.Release.Namespace}}.{{$trustDomain}} +- name: LINKERD2_PROXY_IDENTITY_SVC_NAME + value: linkerd-identity.{{.Release.Namespace}}.serviceaccount.identity.{{.Release.Namespace}}.{{$trustDomain}} +- name: LINKERD2_PROXY_DESTINATION_SVC_NAME + value: linkerd-destination.{{.Release.Namespace}}.serviceaccount.identity.{{.Release.Namespace}}.{{$trustDomain}} +- name: LINKERD2_PROXY_POLICY_SVC_NAME + value: linkerd-destination.{{.Release.Namespace}}.serviceaccount.identity.{{.Release.Namespace}}.{{$trustDomain}} +{{ if .Values.proxy.accessLog -}} +- name: LINKERD2_PROXY_ACCESS_LOG + value: {{.Values.proxy.accessLog | quote}} +{{ end -}} +{{ if .Values.proxy.shutdownGracePeriod -}} +- name: LINKERD2_PROXY_SHUTDOWN_GRACE_PERIOD + value: {{.Values.proxy.shutdownGracePeriod | quote}} +{{ end -}} +image: {{.Values.proxy.image.name}}:{{.Values.proxy.image.version | default .Values.linkerdVersion}} +imagePullPolicy: {{.Values.proxy.image.pullPolicy | default .Values.imagePullPolicy}} +livenessProbe: + httpGet: + path: /live + port: {{.Values.proxy.ports.admin}} + initialDelaySeconds: 10 +name: linkerd-proxy +ports: +- containerPort: {{.Values.proxy.ports.inbound}} + name: linkerd-proxy +- containerPort: {{.Values.proxy.ports.admin}} + name: linkerd-admin +readinessProbe: + httpGet: + path: /ready + port: {{.Values.proxy.ports.admin}} + initialDelaySeconds: 2 +{{- if .Values.proxy.resources }} +{{ include "partials.resources" .Values.proxy.resources }} +{{- end }} +securityContext: + allowPrivilegeEscalation: false + {{- if .Values.proxy.capabilities -}} + {{- include "partials.proxy.capabilities" . | nindent 2 -}} + {{- end }} + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: {{.Values.proxy.uid}} + seccompProfile: + type: RuntimeDefault +terminationMessagePolicy: FallbackToLogsOnError +{{- if or (.Values.proxy.await) (.Values.proxy.waitBeforeExitSeconds) }} +lifecycle: +{{- if .Values.proxy.await }} + postStart: + exec: + command: + - /usr/lib/linkerd/linkerd-await + - --timeout=2m + - --port={{.Values.proxy.ports.admin}} +{{- end }} +{{- if .Values.proxy.waitBeforeExitSeconds }} + preStop: + exec: + command: + - /bin/sleep + - {{.Values.proxy.waitBeforeExitSeconds | quote}} +{{- end }} +{{- end }} +volumeMounts: +- mountPath: /var/run/linkerd/identity/end-entity + name: linkerd-identity-end-entity +{{- if .Values.identity.serviceAccountTokenProjection }} +- mountPath: /var/run/secrets/tokens + name: linkerd-identity-token +{{- end }} +{{- if .Values.proxy.saMountPath }} +- mountPath: {{.Values.proxy.saMountPath.mountPath}} + name: {{.Values.proxy.saMountPath.name}} + readOnly: {{.Values.proxy.saMountPath.readOnly}} +{{- end -}} +{{- end }} diff --git a/linkerd/charts/linkerd-crds/charts/partials/templates/_pull-secrets.tpl b/linkerd/charts/linkerd-crds/charts/partials/templates/_pull-secrets.tpl new file mode 100644 index 0000000..0c9aa4f --- /dev/null +++ b/linkerd/charts/linkerd-crds/charts/partials/templates/_pull-secrets.tpl @@ -0,0 +1,6 @@ +{{- define "partials.image-pull-secrets"}} +{{- if . }} +imagePullSecrets: +{{ toYaml . | indent 2 }} +{{- end }} +{{- end -}} diff --git a/linkerd/charts/linkerd-crds/charts/partials/templates/_resources.tpl b/linkerd/charts/linkerd-crds/charts/partials/templates/_resources.tpl new file mode 100644 index 0000000..1fd6789 --- /dev/null +++ b/linkerd/charts/linkerd-crds/charts/partials/templates/_resources.tpl @@ -0,0 +1,28 @@ +{{- define "partials.resources" -}} +{{- $ephemeralStorage := index . "ephemeral-storage" -}} +resources: + {{- if or (.cpu).limit (.memory).limit ($ephemeralStorage).limit }} + limits: + {{- with (.cpu).limit }} + cpu: {{. | quote}} + {{- end }} + {{- with (.memory).limit }} + memory: {{. | quote}} + {{- end }} + {{- with ($ephemeralStorage).limit }} + ephemeral-storage: {{. | quote}} + {{- end }} + {{- end }} + {{- if or (.cpu).request (.memory).request ($ephemeralStorage).request }} + requests: + {{- with (.cpu).request }} + cpu: {{. | quote}} + {{- end }} + {{- with (.memory).request }} + memory: {{. | quote}} + {{- end }} + {{- with ($ephemeralStorage).request }} + ephemeral-storage: {{. | quote}} + {{- end }} + {{- end }} +{{- end }} diff --git a/linkerd/charts/linkerd-crds/charts/partials/templates/_tolerations.tpl b/linkerd/charts/linkerd-crds/charts/partials/templates/_tolerations.tpl new file mode 100644 index 0000000..c2292b1 --- /dev/null +++ b/linkerd/charts/linkerd-crds/charts/partials/templates/_tolerations.tpl @@ -0,0 +1,4 @@ +{{- define "linkerd.tolerations" -}} +tolerations: +{{ toYaml .Values.tolerations | trim | indent 2 }} +{{- end -}} diff --git a/linkerd/charts/linkerd-crds/charts/partials/templates/_trace.tpl b/linkerd/charts/linkerd-crds/charts/partials/templates/_trace.tpl new file mode 100644 index 0000000..dee0595 --- /dev/null +++ b/linkerd/charts/linkerd-crds/charts/partials/templates/_trace.tpl @@ -0,0 +1,5 @@ +{{ define "partials.linkerd.trace" -}} +{{ if .Values.controlPlaneTracing -}} +- -trace-collector=collector.{{.Values.controlPlaneTracingNamespace}}.svc.{{.Values.clusterDomain}}:55678 +{{ end -}} +{{- end }} diff --git a/linkerd/charts/linkerd-crds/charts/partials/templates/_validate.tpl b/linkerd/charts/linkerd-crds/charts/partials/templates/_validate.tpl new file mode 100644 index 0000000..ba772c2 --- /dev/null +++ b/linkerd/charts/linkerd-crds/charts/partials/templates/_validate.tpl @@ -0,0 +1,19 @@ +{{- define "linkerd.webhook.validation" -}} + +{{- if and (.injectCaFrom) (.injectCaFromSecret) -}} +{{- fail "injectCaFrom and injectCaFromSecret cannot both be set" -}} +{{- end -}} + +{{- if and (or (.injectCaFrom) (.injectCaFromSecret)) (.caBundle) -}} +{{- fail "injectCaFrom or injectCaFromSecret cannot be set if providing a caBundle" -}} +{{- end -}} + +{{- if and (.externalSecret) (empty .caBundle) (empty .injectCaFrom) (empty .injectCaFromSecret) -}} +{{- fail "if externalSecret is set, then caBundle, injectCaFrom, or injectCaFromSecret must be set" -}} +{{- end }} + +{{- if and (or .injectCaFrom .injectCaFromSecret .caBundle) (not .externalSecret) -}} +{{- fail "if caBundle, injectCaFrom, or injectCaFromSecret is set, then externalSecret must be set" -}} +{{- end -}} + +{{- end -}} diff --git a/linkerd/charts/linkerd-crds/charts/partials/templates/_volumes.tpl b/linkerd/charts/linkerd-crds/charts/partials/templates/_volumes.tpl new file mode 100644 index 0000000..9684cf2 --- /dev/null +++ b/linkerd/charts/linkerd-crds/charts/partials/templates/_volumes.tpl @@ -0,0 +1,20 @@ +{{ define "partials.proxy.volumes.identity" -}} +emptyDir: + medium: Memory +name: linkerd-identity-end-entity +{{- end -}} + +{{ define "partials.proxyInit.volumes.xtables" -}} +emptyDir: {} +name: {{ .Values.proxyInit.xtMountPath.name }} +{{- end -}} + +{{- define "partials.proxy.volumes.service-account-token" -}} +name: linkerd-identity-token +projected: + sources: + - serviceAccountToken: + path: linkerd-identity-token + expirationSeconds: 86400 {{- /* # 24 hours */}} + audience: identity.l5d.io +{{- end -}} diff --git a/linkerd/charts/linkerd-crds/charts/partials/values.yaml b/linkerd/charts/linkerd-crds/charts/partials/values.yaml new file mode 100644 index 0000000..e69de29 diff --git a/linkerd/charts/linkerd-crds/templates/NOTES.txt b/linkerd/charts/linkerd-crds/templates/NOTES.txt new file mode 100644 index 0000000..4ff5c18 --- /dev/null +++ b/linkerd/charts/linkerd-crds/templates/NOTES.txt @@ -0,0 +1,6 @@ +The linkerd-crds chart was successfully installed 🎉 + +To complete the linkerd core installation, please now proceed to install the +linkerd-control-plane chart in the {{ .Release.Namespace }} namespace. + +Looking for more? Visit https://linkerd.io/2/getting-started/ diff --git a/linkerd/charts/linkerd-crds/templates/gateway.networking.k8s.io_httproutes.yaml b/linkerd/charts/linkerd-crds/templates/gateway.networking.k8s.io_httproutes.yaml new file mode 100644 index 0000000..2a88ae2 --- /dev/null +++ b/linkerd/charts/linkerd-crds/templates/gateway.networking.k8s.io_httproutes.yaml @@ -0,0 +1,4012 @@ +{{- if .Values.enableHttpRoutes }} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/1923 + gateway.networking.k8s.io/bundle-version: v0.7.1-dev + gateway.networking.k8s.io/channel: experimental + {{ include "partials.annotations.created-by" . }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + linkerd.io/control-plane-ns: {{.Release.Namespace}} + creationTimestamp: null + name: httproutes.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: HTTPRoute + listKind: HTTPRouteList + plural: httproutes + singular: httproute + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.hostnames + name: Hostnames + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + deprecated: true + deprecationWarning: The v1alpha2 version of HTTPRoute has been deprecated and + will be removed in a future release of the API. Please upgrade to v1beta1. + name: v1alpha2 + schema: + openAPIV3Schema: + description: HTTPRoute provides a way to route HTTP requests. This includes + the capability to match requests by hostname, path, header, or query param. + Filters can be used to specify additional processing steps. Backends specify + where matching requests should be routed. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of HTTPRoute. + properties: + hostnames: + description: "Hostnames defines a set of hostname that should match + against the HTTP Host header to select a HTTPRoute used to process + the request. Implementations MUST ignore any port value specified + in the HTTP Host header while performing a match and (absent of + any applicable header modification configuration) MUST forward this + header unmodified to the backend. \n Valid values for Hostnames + are determined by RFC 1123 definition of a hostname with 2 notable + exceptions: \n 1. IPs are not allowed. 2. A hostname may be prefixed + with a wildcard label (`*.`). The wildcard label must appear by + itself as the first label. \n If a hostname is specified by both + the Listener and HTTPRoute, there must be at least one intersecting + hostname for the HTTPRoute to be attached to the Listener. For example: + \n * A Listener with `test.example.com` as the hostname matches + HTTPRoutes that have either not specified any hostnames, or have + specified at least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches HTTPRoutes + that have either not specified any hostnames or have specified at + least one hostname that matches the Listener hostname. For example, + `*.example.com`, `test.example.com`, and `foo.test.example.com` + would all match. On the other hand, `example.com` and `test.example.net` + would not match. \n Hostnames that are prefixed with a wildcard + label (`*.`) are interpreted as a suffix match. That means that + a match for `*.example.com` would match both `test.example.com`, + and `foo.test.example.com`, but not `example.com`. \n If both the + Listener and HTTPRoute have specified hostnames, any HTTPRoute hostnames + that do not match the Listener hostname MUST be ignored. For example, + if a Listener specified `*.example.com`, and the HTTPRoute specified + `test.example.com` and `test.example.net`, `test.example.net` must + not be considered for a match. \n If both the Listener and HTTPRoute + have specified hostnames, and none match with the criteria above, + then the HTTPRoute is not accepted. The implementation must raise + an 'Accepted' Condition with a status of `False` in the corresponding + RouteParentStatus. \n In the event that multiple HTTPRoutes specify + intersecting hostnames (e.g. overlapping wildcard matching and exact + matching hostnames), precedence must be given to rules from the + HTTPRoute with the largest number of: \n * Characters in a matching + non-wildcard hostname. * Characters in a matching hostname. \n If + ties exist across multiple Routes, the matching precedence rules + for HTTPRouteMatches takes over. \n Support: Core" + items: + description: "Hostname is the fully qualified domain name of a network + host. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname + may be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n Hostname can be \"precise\" + which is a domain name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", which is a domain + name prefixed with a single wildcard label (e.g. `*.example.com`). + \n Note that as per RFC1035 and RFC1123, a *label* must consist + of lower case alphanumeric characters or '-', and must start and + end with an alphanumeric character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + parentRefs: + description: "ParentRefs references the resources (usually Gateways) + that a Route wants to be attached to. Note that the referenced parent + resource needs to allow this for the attachment to be complete. + For Gateways, that means the Gateway needs to allow attachment from + Routes of this kind and namespace. \n The only kind of parent resource + with \"Core\" support is Gateway. This API may be extended in the + future to support additional kinds of parent resources such as one + of the route kinds. \n It is invalid to reference an identical parent + more than once. It is valid to reference multiple distinct sections + within the same parent resource, such as 2 Listeners within a Gateway. + \n It is possible to separately reference multiple distinct objects + that may be collapsed by an implementation. For example, some implementations + may choose to merge compatible Gateway Listeners together. If that + is the case, the list of routes attached to those resources should + also be merged. \n Note that for ParentRefs that cross namespace + boundaries, there are specific rules. Cross-namespace references + are only valid if they are explicitly allowed by something in the + namespace they are referring to. For example, Gateway has the AllowedRoutes + field, and ReferenceGrant provides a generic way to enable any other + kind of cross-namespace reference." + items: + description: "ParentReference identifies an API object (usually + a Gateway) that can be considered a parent of this resource (usually + a route). The only kind of parent resource with \"Core\" support + is Gateway. This API may be extended in the future to support + additional kinds of parent resources, such as HTTPRoute. \n The + API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid." + properties: + group: + default: gateway.networking.k8s.io + description: "Group is the group of the referent. When unspecified, + \"gateway.networking.k8s.io\" is inferred. To set the core + API group (such as for a \"Service\" kind referent), Group + must be explicitly set to \"\" (empty string). \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: Core + (Gateway) \n Support: Implementation-specific (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. When + unspecified, this refers to the local namespace of the Route. + \n Note that there are specific rules for ParentRefs which + cross namespace boundaries. Cross-namespace references are + only valid if they are explicitly allowed by something in + the namespace they are referring to. For example: Gateway + has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: "Port is the network port this Route targets. It + can be interpreted differently based on the type of parent + resource. \n When the parent resource is a Gateway, this targets + all listeners listening on the specified port that also support + this kind of Route(and select this Route). It's not recommended + to set `Port` unless the networking behaviors specified in + a Route must apply to a specific port as opposed to a listener(s) + whose port(s) may be changed. When both Port and SectionName + are specified, the name and port of the selected listener + must match both specified values. \n Implementations MAY choose + to support other parent resources. Implementations supporting + other types of parent resources MUST clearly document how/if + Port is interpreted. \n For the purpose of status, an attachment + is considered successful as long as the parent resource accepts + it partially. For example, Gateway listeners can restrict + which Routes can attach to them by Route kind, namespace, + or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this + Route, the Route MUST be considered detached from the Gateway. + \n Support: Extended \n " + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: "SectionName is the name of a section within the + target resource. In the following resources, SectionName is + interpreted as the following: \n * Gateway: Listener Name. + When both Port (experimental) and SectionName are specified, + the name and port of the selected listener must match both + specified values. \n Implementations MAY choose to support + attaching Routes to other resources. If that is the case, + they MUST clearly document how SectionName is interpreted. + \n When unspecified (empty string), this will reference the + entire resource. For the purpose of status, an attachment + is considered successful if at least one section in the parent + resource accepts it. For example, Gateway listeners can restrict + which Routes can attach to them by Route kind, namespace, + or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this + Route, the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + rules: + default: + - matches: + - path: + type: PathPrefix + value: / + description: Rules are a list of HTTP matchers, filters and actions. + items: + description: HTTPRouteRule defines semantics for matching an HTTP + request based on conditions (matches), processing it (filters), + and forwarding the request to an API object (backendRefs). + properties: + backendRefs: + description: "BackendRefs defines the backend(s) where matching + requests should be sent. \n Failure behavior here depends + on how many BackendRefs are specified and how many are invalid. + \n If *all* entries in BackendRefs are invalid, and there + are also no filters specified in this route rule, *all* traffic + which matches this rule MUST receive a 500 status code. \n + See the HTTPBackendRef definition for the rules about what + makes a single HTTPBackendRef invalid. \n When a HTTPBackendRef + is invalid, 500 status codes MUST be returned for requests + that would have otherwise been routed to an invalid backend. + If multiple backends are specified, and some are invalid, + the proportion of requests that would otherwise have been + routed to an invalid backend MUST receive a 500 status code. + \n For example, if two backends are specified with equal weights, + and one is invalid, 50 percent of traffic must receive a 500. + Implementations may choose how that 50 percent is determined. + \n Support: Core for Kubernetes Service \n Support: Extended + for Kubernetes ServiceImport \n Support: Implementation-specific + for any other resource \n Support for weight: Core" + items: + description: HTTPBackendRef defines how a HTTPRoute should + forward an HTTP request. + properties: + filters: + description: "Filters defined at this level should be + executed if and only if the request is being forwarded + to the backend defined here. \n Support: Implementation-specific + (For broader support of filters, use the Filters field + in HTTPRouteRule.)" + items: + description: HTTPRouteFilter defines processing steps + that must be completed during the request or response + lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway + implementations. Some examples include request or + response modification, implementing authentication + strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type + of the filter. + properties: + extensionRef: + description: "ExtensionRef is an optional, implementation-specific + extension to the \"filter\" behavior. For example, + resource \"myroutefilter\" in group \"networking.example.net\"). + ExtensionRef MUST NOT be used for core and extended + filters. \n Support: Implementation-specific" + properties: + group: + description: Group is the group of the referent. + For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API + group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For + example "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema + for a filter that modifies request headers. \n + Support: Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: "RequestMirror defines a schema for + a filter that mirrors requests. Requests are sent + to the specified destination, but responses from + that destination are ignored. \n Support: Extended" + properties: + backendRef: + description: "BackendRef references a resource + where mirrored requests are sent. \n If the + referent cannot be found, this BackendRef + is invalid and must be dropped from the Gateway. + The controller must ensure the \"ResolvedRefs\" + condition on the Route status is set to `status: + False` and not configure this backend in the + underlying implementation. \n If there is + a cross-namespace reference to an *existing* + object that is not allowed by a ReferenceGrant, + the controller must ensure the \"ResolvedRefs\" + \ condition on the Route is set to `status: + False`, with the \"RefNotPermitted\" reason + and not configure this backend in the underlying + implementation. \n In either error case, the + Message of the `ResolvedRefs` Condition should + be used to provide more detail about the problem. + \n Support: Extended for Kubernetes Service + \n Support: Implementation-specific for any + other resource" + properties: + group: + default: "" + description: Group is the group of the referent. + For example, "gateway.networking.k8s.io". + When unspecified or empty string, core + API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: "Kind is the Kubernetes resource + kind of the referent. For example \"Service\". + \n Defaults to \"Service\" when not specified. + \n ExternalName services can refer to + CNAME DNS records that may live outside + of the cluster and as such are difficult + to reason about in terms of conformance. + They also may not be safe to forward to + (see CVE-2021-25740 for more information). + Implementations SHOULD NOT support ExternalName + Services. \n Support: Core (Services with + a type other than ExternalName) \n Support: + Implementation-specific (Services with + type ExternalName)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace + of the backend. When unspecified, the + local namespace is inferred. \n Note that + when a namespace different than the local + namespace is specified, a ReferenceGrant + object is required in the referent namespace + to allow that namespace's owner to accept + the reference. See the ReferenceGrant + documentation for details. \n Support: + Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: Port specifies the destination + port number to use for this resource. + Port is required when the referent is + a Kubernetes Service. In this case, the + port number is the service port number, + not the target port. For other resources, + destination port might be derived from + the referent resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + required: + - backendRef + type: object + requestRedirect: + description: "RequestRedirect defines a schema for + a filter that responds to the request with an + HTTP redirection. \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be + used in the value of the `Location` header + in the response. When empty, the hostname + in the `Host` header of the request is used. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to + modify the path of the incoming request. The + modified path is then used to construct the + `Location` header. When empty, the request + path is used as-is. \n Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in + the value of the `Location` header in the + response. \n If no port is specified, the + redirect port MUST be derived using the following + rules: \n * If redirect scheme is not-empty, + the redirect port MUST be the well-known port + associated with the redirect scheme. Specifically + \"http\" to port 80 and \"https\" to port + 443. If the redirect scheme does not have + a well-known port, the listener port of the + Gateway SHOULD be used. * If redirect scheme + is empty, the redirect port MUST be the Gateway + Listener port. \n Implementations SHOULD NOT + add the port number in the 'Location' header + in the following cases: \n * A Location header + that will use HTTP (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 80. * A Location header that + will use HTTPS (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 443. \n Support: Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used + in the value of the `Location` header in the + response. When empty, the scheme of the request + is used. \n Scheme redirects can affect the + port of the redirect, for more information, + refer to the documentation for the port field + of this filter. \n Note that values may be + added to this enum, implementations must ensure + that unknown values will not cause a crash. + \n Unknown values here must result in the + implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`. \n Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status + code to be used in response. \n Note that + values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result + in the implementation setting the Accepted + Condition for the Route to `status: False`, + with a Reason of `UnsupportedValue`. \n Support: + Core" + enum: + - 301 + - 302 + type: integer + type: object + responseHeaderModifier: + description: "ResponseHeaderModifier defines a schema + for a filter that modifies response headers. \n + Support: Extended" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: "Type identifies the type of filter + to apply. As with other API fields, types are + classified into three conformance levels: \n - + Core: Filter types and their corresponding configuration + defined by \"Support: Core\" in this package, + e.g. \"RequestHeaderModifier\". All implementations + must support core filters. \n - Extended: Filter + types and their corresponding configuration defined + by \"Support: Extended\" in this package, e.g. + \"RequestMirror\". Implementers are encouraged + to support extended filters. \n - Implementation-specific: + Filters that are defined and supported by specific + vendors. In the future, filters showing convergence + in behavior across multiple implementations will + be considered for inclusion in extended or core + conformance levels. Filter-specific configuration + for such filters is specified using the ExtensionRef + field. `Type` should be set to \"ExtensionRef\" + for custom filters. \n Implementers are encouraged + to define custom implementation types to extend + the core API with implementation-specific behavior. + \n If a reference to a custom filter type cannot + be resolved, the filter MUST NOT be skipped. Instead, + requests that would have been processed by that + filter MUST receive a HTTP error response. \n + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result in + the implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestMirror + - RequestRedirect + - URLRewrite + - ExtensionRef + type: string + urlRewrite: + description: "URLRewrite defines a schema for a + filter that modifies a request during forwarding. + \n Support: Extended" + properties: + hostname: + description: "Hostname is the value to be used + to replace the Host header value during forwarding. + \n Support: Extended" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines a path rewrite. \n + Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + type: object + required: + - type + type: object + maxItems: 16 + type: array + group: + default: "" + description: Group is the group of the referent. For example, + "gateway.networking.k8s.io". When unspecified or empty + string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: "Kind is the Kubernetes resource kind of + the referent. For example \"Service\". \n Defaults to + \"Service\" when not specified. \n ExternalName services + can refer to CNAME DNS records that may live outside + of the cluster and as such are difficult to reason about + in terms of conformance. They also may not be safe to + forward to (see CVE-2021-25740 for more information). + Implementations SHOULD NOT support ExternalName Services. + \n Support: Core (Services with a type other than ExternalName) + \n Support: Implementation-specific (Services with type + ExternalName)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the backend. + When unspecified, the local namespace is inferred. \n + Note that when a namespace different than the local + namespace is specified, a ReferenceGrant object is required + in the referent namespace to allow that namespace's + owner to accept the reference. See the ReferenceGrant + documentation for details. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: Port specifies the destination port number + to use for this resource. Port is required when the + referent is a Kubernetes Service. In this case, the + port number is the service port number, not the target + port. For other resources, destination port might be + derived from the referent resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: "Weight specifies the proportion of requests + forwarded to the referenced backend. This is computed + as weight/(sum of all weights in this BackendRefs list). + For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision + an implementation supports. Weight is not a percentage + and the sum of weights does not need to equal 100. \n + If only one backend is specified and it has a weight + greater than 0, 100% of the traffic is forwarded to + that backend. If weight is set to 0, no traffic should + be forwarded for this entry. If unspecified, weight + defaults to 1. \n Support for this field varies based + on the context where used." + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + maxItems: 16 + type: array + filters: + description: "Filters define the filters that are applied to + requests that match this rule. \n The effects of ordering + of multiple behaviors are currently unspecified. This can + change in the future based on feedback during the alpha stage. + \n Conformance-levels at this level are defined based on the + type of filter: \n - ALL core filters MUST be supported by + all implementations. - Implementers are encouraged to support + extended filters. - Implementation-specific custom filters + have no API guarantees across implementations. \n Specifying + a core filter multiple times has unspecified or implementation-specific + conformance. \n All filters are expected to be compatible + with each other except for the URLRewrite and RequestRedirect + filters, which may not be combined. If an implementation can + not support other combinations of filters, they must clearly + document that limitation. In all cases where incompatible + or unsupported filters are specified, implementations MUST + add a warning condition to status. \n Support: Core" + items: + description: HTTPRouteFilter defines processing steps that + must be completed during the request or response lifecycle. + HTTPRouteFilters are meant as an extension point to express + processing that may be done in Gateway implementations. + Some examples include request or response modification, + implementing authentication strategies, rate-limiting, and + traffic shaping. API guarantee/conformance is defined based + on the type of the filter. + properties: + extensionRef: + description: "ExtensionRef is an optional, implementation-specific + extension to the \"filter\" behavior. For example, + resource \"myroutefilter\" in group \"networking.example.net\"). + ExtensionRef MUST NOT be used for core and extended + filters. \n Support: Implementation-specific" + properties: + group: + description: Group is the group of the referent. For + example, "gateway.networking.k8s.io". When unspecified + or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For example + "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema for + a filter that modifies request headers. \n Support: + Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It appends + to any existing values associated with the header + name. \n Input: GET /foo HTTP/1.1 my-header: foo + \n Config: add: - name: \"my-header\" value: \"bar,baz\" + \n Output: GET /foo HTTP/1.1 my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from the + HTTP request before the action. The value of Remove + is a list of HTTP header names. Note that the header + names are case-insensitive (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo my-header2: + bar my-header3: baz \n Config: remove: [\"my-header1\", + \"my-header3\"] \n Output: GET /foo HTTP/1.1 my-header2: + bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with the + given header (name, value) before the action. \n + Input: GET /foo HTTP/1.1 my-header: foo \n Config: + set: - name: \"my-header\" value: \"bar\" \n Output: + GET /foo HTTP/1.1 my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: "RequestMirror defines a schema for a filter + that mirrors requests. Requests are sent to the specified + destination, but responses from that destination are + ignored. \n Support: Extended" + properties: + backendRef: + description: "BackendRef references a resource where + mirrored requests are sent. \n If the referent cannot + be found, this BackendRef is invalid and must be + dropped from the Gateway. The controller must ensure + the \"ResolvedRefs\" condition on the Route status + is set to `status: False` and not configure this + backend in the underlying implementation. \n If + there is a cross-namespace reference to an *existing* + object that is not allowed by a ReferenceGrant, + the controller must ensure the \"ResolvedRefs\" + \ condition on the Route is set to `status: False`, + with the \"RefNotPermitted\" reason and not configure + this backend in the underlying implementation. \n + In either error case, the Message of the `ResolvedRefs` + Condition should be used to provide more detail + about the problem. \n Support: Extended for Kubernetes + Service \n Support: Implementation-specific for + any other resource" + properties: + group: + default: "" + description: Group is the group of the referent. + For example, "gateway.networking.k8s.io". When + unspecified or empty string, core API group + is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: "Kind is the Kubernetes resource + kind of the referent. For example \"Service\". + \n Defaults to \"Service\" when not specified. + \n ExternalName services can refer to CNAME + DNS records that may live outside of the cluster + and as such are difficult to reason about in + terms of conformance. They also may not be safe + to forward to (see CVE-2021-25740 for more information). + Implementations SHOULD NOT support ExternalName + Services. \n Support: Core (Services with a + type other than ExternalName) \n Support: Implementation-specific + (Services with type ExternalName)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the + backend. When unspecified, the local namespace + is inferred. \n Note that when a namespace different + than the local namespace is specified, a ReferenceGrant + object is required in the referent namespace + to allow that namespace's owner to accept the + reference. See the ReferenceGrant documentation + for details. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: Port specifies the destination port + number to use for this resource. Port is required + when the referent is a Kubernetes Service. In + this case, the port number is the service port + number, not the target port. For other resources, + destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + required: + - backendRef + type: object + requestRedirect: + description: "RequestRedirect defines a schema for a filter + that responds to the request with an HTTP redirection. + \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be used + in the value of the `Location` header in the response. + When empty, the hostname in the `Host` header of + the request is used. \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to modify + the path of the incoming request. The modified path + is then used to construct the `Location` header. + When empty, the request path is used as-is. \n Support: + Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the value + with which to replace the full path of a request + during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies the + value with which to replace the prefix match + of a request during a rewrite or redirect. For + example, a request to \"/foo/bar\" with a prefix + match of \"/foo\" and a ReplacePrefixMatch of + \"/xyz\" would be modified to \"/xyz/bar\". + \n Note that this matches the behavior of the + PathPrefix match type. This matches full path + elements. A path element refers to the list + of labels in the path split by the `/` separator. + When specified, a trailing `/` is ignored. For + example, the paths `/abc`, `/abc/`, and `/abc/def` + would all match the prefix `/abc`, but the path + `/abcd` would not. \n Request Path | Prefix + Match | Replace Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | /xyz/ + \ | /xyz/bar /foo/bar | /foo/ | + /xyz | /xyz/bar /foo/bar | /foo/ + \ | /xyz/ | /xyz/bar /foo | + /foo | /xyz | /xyz /foo/ | + /foo | /xyz | /xyz/ /foo/bar + \ | /foo | | /bar + /foo/ | /foo | + | / /foo | /foo | + | / /foo/ | /foo | / | + / /foo | /foo | / | + /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path modifier. + Additional types may be added in a future release + of the API. \n Note that values may be added + to this enum, implementations must ensure that + unknown values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the Route + to `status: False`, with a Reason of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in the value + of the `Location` header in the response. \n If + no port is specified, the redirect port MUST be + derived using the following rules: \n * If redirect + scheme is not-empty, the redirect port MUST be the + well-known port associated with the redirect scheme. + Specifically \"http\" to port 80 and \"https\" to + port 443. If the redirect scheme does not have a + well-known port, the listener port of the Gateway + SHOULD be used. * If redirect scheme is empty, the + redirect port MUST be the Gateway Listener port. + \n Implementations SHOULD NOT add the port number + in the 'Location' header in the following cases: + \n * A Location header that will use HTTP (whether + that is determined via the Listener protocol or + the Scheme field) _and_ use port 80. * A Location + header that will use HTTPS (whether that is determined + via the Listener protocol or the Scheme field) _and_ + use port 443. \n Support: Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used in the + value of the `Location` header in the response. + When empty, the scheme of the request is used. \n + Scheme redirects can affect the port of the redirect, + for more information, refer to the documentation + for the port field of this filter. \n Note that + values may be added to this enum, implementations + must ensure that unknown values will not cause a + crash. \n Unknown values here must result in the + implementation setting the Accepted Condition for + the Route to `status: False`, with a Reason of `UnsupportedValue`. + \n Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status code to + be used in response. \n Note that values may be + added to this enum, implementations must ensure + that unknown values will not cause a crash. \n Unknown + values here must result in the implementation setting + the Accepted Condition for the Route to `status: + False`, with a Reason of `UnsupportedValue`. \n + Support: Core" + enum: + - 301 + - 302 + type: integer + type: object + responseHeaderModifier: + description: "ResponseHeaderModifier defines a schema + for a filter that modifies response headers. \n Support: + Extended" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It appends + to any existing values associated with the header + name. \n Input: GET /foo HTTP/1.1 my-header: foo + \n Config: add: - name: \"my-header\" value: \"bar,baz\" + \n Output: GET /foo HTTP/1.1 my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from the + HTTP request before the action. The value of Remove + is a list of HTTP header names. Note that the header + names are case-insensitive (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo my-header2: + bar my-header3: baz \n Config: remove: [\"my-header1\", + \"my-header3\"] \n Output: GET /foo HTTP/1.1 my-header2: + bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with the + given header (name, value) before the action. \n + Input: GET /foo HTTP/1.1 my-header: foo \n Config: + set: - name: \"my-header\" value: \"bar\" \n Output: + GET /foo HTTP/1.1 my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: "Type identifies the type of filter to apply. + As with other API fields, types are classified into + three conformance levels: \n - Core: Filter types and + their corresponding configuration defined by \"Support: + Core\" in this package, e.g. \"RequestHeaderModifier\". + All implementations must support core filters. \n - + Extended: Filter types and their corresponding configuration + defined by \"Support: Extended\" in this package, e.g. + \"RequestMirror\". Implementers are encouraged to support + extended filters. \n - Implementation-specific: Filters + that are defined and supported by specific vendors. + In the future, filters showing convergence in behavior + across multiple implementations will be considered for + inclusion in extended or core conformance levels. Filter-specific + configuration for such filters is specified using the + ExtensionRef field. `Type` should be set to \"ExtensionRef\" + for custom filters. \n Implementers are encouraged to + define custom implementation types to extend the core + API with implementation-specific behavior. \n If a reference + to a custom filter type cannot be resolved, the filter + MUST NOT be skipped. Instead, requests that would have + been processed by that filter MUST receive a HTTP error + response. \n Note that values may be added to this enum, + implementations must ensure that unknown values will + not cause a crash. \n Unknown values here must result + in the implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason of `UnsupportedValue`." + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestMirror + - RequestRedirect + - URLRewrite + - ExtensionRef + type: string + urlRewrite: + description: "URLRewrite defines a schema for a filter + that modifies a request during forwarding. \n Support: + Extended" + properties: + hostname: + description: "Hostname is the value to be used to + replace the Host header value during forwarding. + \n Support: Extended" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines a path rewrite. \n Support: + Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the value + with which to replace the full path of a request + during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies the + value with which to replace the prefix match + of a request during a rewrite or redirect. For + example, a request to \"/foo/bar\" with a prefix + match of \"/foo\" and a ReplacePrefixMatch of + \"/xyz\" would be modified to \"/xyz/bar\". + \n Note that this matches the behavior of the + PathPrefix match type. This matches full path + elements. A path element refers to the list + of labels in the path split by the `/` separator. + When specified, a trailing `/` is ignored. For + example, the paths `/abc`, `/abc/`, and `/abc/def` + would all match the prefix `/abc`, but the path + `/abcd` would not. \n Request Path | Prefix + Match | Replace Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | /xyz/ + \ | /xyz/bar /foo/bar | /foo/ | + /xyz | /xyz/bar /foo/bar | /foo/ + \ | /xyz/ | /xyz/bar /foo | + /foo | /xyz | /xyz /foo/ | + /foo | /xyz | /xyz/ /foo/bar + \ | /foo | | /bar + /foo/ | /foo | + | / /foo | /foo | + | / /foo/ | /foo | / | + / /foo | /foo | / | + /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path modifier. + Additional types may be added in a future release + of the API. \n Note that values may be added + to this enum, implementations must ensure that + unknown values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the Route + to `status: False`, with a Reason of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + type: object + required: + - type + type: object + maxItems: 16 + type: array + matches: + default: + - path: + type: PathPrefix + value: / + description: "Matches define conditions used for matching the + rule against incoming HTTP requests. Each match is independent, + i.e. this rule will be matched if **any** one of the matches + is satisfied. \n For example, take the following matches configuration: + \n ``` matches: - path: value: \"/foo\" headers: - name: \"version\" + value: \"v2\" - path: value: \"/v2/foo\" ``` \n For a request + to match against this rule, a request must satisfy EITHER + of the two conditions: \n - path prefixed with `/foo` AND + contains the header `version: v2` - path prefix of `/v2/foo` + \n See the documentation for HTTPRouteMatch on how to specify + multiple match conditions that should be ANDed together. \n + If no matches are specified, the default is a prefix path + match on \"/\", which has the effect of matching every HTTP + request. \n Proxy or Load Balancer routing configuration generated + from HTTPRoutes MUST prioritize matches based on the following + criteria, continuing on ties. Across all rules specified on + applicable Routes, precedence must be given to the match having: + \n * \"Exact\" path match. * \"Prefix\" path match with largest + number of characters. * Method match. * Largest number of + header matches. * Largest number of query param matches. \n + Note: The precedence of RegularExpression path matches are + implementation-specific. \n If ties still exist across multiple + Routes, matching precedence MUST be determined in order of + the following criteria, continuing on ties: \n * The oldest + Route based on creation timestamp. * The Route appearing first + in alphabetical order by \"{namespace}/{name}\". \n If ties + still exist within an HTTPRoute, matching precedence MUST + be granted to the FIRST matching rule (in list order) with + a match meeting the above criteria. \n When no rules matching + a request have been successfully attached to the parent a + request is coming from, a HTTP 404 status code MUST be returned." + items: + description: "HTTPRouteMatch defines the predicate used to + match requests to a given action. Multiple match types are + ANDed together, i.e. the match will evaluate to true only + if all conditions are satisfied. \n For example, the match + below will match a HTTP request only if its path starts + with `/foo` AND it contains the `version: v1` header: \n + ``` match: \n path: value: \"/foo\" headers: - name: \"version\" + value \"v1\" \n ```" + properties: + headers: + description: Headers specifies HTTP request header matchers. + Multiple match values are ANDed together, meaning, a + request must match all the specified headers to select + the route. + items: + description: HTTPHeaderMatch describes how to select + a HTTP route by matching HTTP request headers. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case insensitive. + (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent header + names, only the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST be + ignored. Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered equivalent. + \n When a header is repeated in an HTTP request, + it is implementation-specific behavior as to how + this is represented. Generally, proxies should + follow the guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, with special + handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the header. \n Support: Core (Exact) + \n Support: Implementation-specific (RegularExpression) + \n Since RegularExpression HeaderMatchType has + implementation-specific conformance, implementations + can support POSIX, PCRE or any other dialects + of regular expressions. Please read the implementation's + documentation to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP Header to + be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. When + specified, this route will be matched only if the request + has the specified method. \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path matcher. + If this field is not specified, a default prefix match + on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match against + the path Value. \n Support: Core (Exact, PathPrefix) + \n Support: Implementation-specific (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match against. + maxLength: 1024 + type: string + type: object + queryParams: + description: "QueryParams specifies HTTP query parameter + matchers. Multiple match values are ANDed together, + meaning, a request must match all the specified query + parameters to select the route. \n Support: Extended" + items: + description: HTTPQueryParamMatch describes how to select + a HTTP route by matching HTTP query parameters. + properties: + name: + description: "Name is the name of the HTTP query + param to be matched. This must be an exact string + match. (See https://tools.ietf.org/html/rfc7230#section-2.7.3). + \n If multiple entries specify equivalent query + param names, only the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent query param name MUST + be ignored. \n If a query param is repeated in + an HTTP request, the behavior is purposely left + undefined, since different data planes have different + capabilities. However, it is *recommended* that + implementations should match against the first + value of the param if the data plane supports + it, as this behavior is expected in other load + balancing contexts outside of the Gateway API. + \n Users SHOULD NOT route traffic based on repeated + query params to guard themselves against potential + differences in the implementations." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the query parameter. \n Support: + Extended (Exact) \n Support: Implementation-specific + (RegularExpression) \n Since RegularExpression + QueryParamMatchType has Implementation-specific + conformance, implementations can support POSIX, + PCRE or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP query param + to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + maxItems: 8 + type: array + type: object + maxItems: 16 + type: array + type: object + status: + description: Status defines the current state of HTTPRoute. + properties: + parents: + description: "Parents is a list of parent resources (usually Gateways) + that are associated with the route, and the status of the route + with respect to each parent. When this route attaches to a parent, + the controller that manages the parent must add an entry to this + list when the controller first sees the route and should update + the entry as appropriate when the route or gateway is modified. + \n Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this + API can only populate Route status for the Gateways/parent resources + they are responsible for. \n A maximum of 32 Gateways will be represented + in this list. An empty list means the route has not been attached + to any Gateway." + items: + description: RouteParentStatus describes the status of a route with + respect to an associated Parent. + properties: + conditions: + description: "Conditions describes the status of the route with + respect to the Gateway. Note that the route's availability + is also subject to the Gateway's own status conditions and + listener status. \n If the Route's ParentRef specifies an + existing Gateway that supports Routes of this kind AND that + Gateway's controller has sufficient access, then that Gateway's + controller MUST set the \"Accepted\" condition on the Route, + to indicate whether the route has been accepted or rejected + by the Gateway, and why. \n A Route MUST be considered \"Accepted\" + if at least one of the Route's rules is implemented by the + Gateway. \n There are a number of cases where the \"Accepted\" + condition may not be set due to lack of controller visibility, + that includes when: \n * The Route refers to a non-existent + parent. * The Route is of a type that the controller does + not support. * The Route is in a namespace the controller + does not have access to." + items: + description: "Condition contains details for one aspect of + the current state of this API Resource. --- This struct + is intended for direct use as an array at the field path + .status.conditions. For example, \n type FooStatus struct{ + // Represents the observations of a foo's current state. + // Known .status.conditions.type are: \"Available\", \"Progressing\", + and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields + }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should + be when the underlying condition changed. If that is + not known, then using the time when the API field changed + is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, + if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the + current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier + indicating the reason for the condition's last transition. + Producers of specific condition types may define expected + values and meanings for this field, and whether the + values are considered a guaranteed API. The value should + be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across + resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability + to deconflict is important. The regex it matches is + (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: "ControllerName is a domain/path string that indicates + the name of the controller that wrote this status. This corresponds + with the controllerName field on GatewayClass. \n Example: + \"example.net/gateway-controller\". \n The format of this + field is DOMAIN \"/\" PATH, where DOMAIN and PATH are valid + Kubernetes names (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + \n Controllers MUST populate this field when writing status. + Controllers should ensure that entries to status populated + with their ControllerName are cleaned up when they are no + longer necessary." + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: ParentRef corresponds with a ParentRef in the spec + that this RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: "Group is the group of the referent. When unspecified, + \"gateway.networking.k8s.io\" is inferred. To set the + core API group (such as for a \"Service\" kind referent), + Group must be explicitly set to \"\" (empty string). \n + Support: Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: + Core (Gateway) \n Support: Implementation-specific (Other + Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. + When unspecified, this refers to the local namespace of + the Route. \n Note that there are specific rules for ParentRefs + which cross namespace boundaries. Cross-namespace references + are only valid if they are explicitly allowed by something + in the namespace they are referring to. For example: Gateway + has the AllowedRoutes field, and ReferenceGrant provides + a generic way to enable any other kind of cross-namespace + reference. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: "Port is the network port this Route targets. + It can be interpreted differently based on the type of + parent resource. \n When the parent resource is a Gateway, + this targets all listeners listening on the specified + port that also support this kind of Route(and select this + Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to + a specific port as opposed to a listener(s) whose port(s) + may be changed. When both Port and SectionName are specified, + the name and port of the selected listener must match + both specified values. \n Implementations MAY choose to + support other parent resources. Implementations supporting + other types of parent resources MUST clearly document + how/if Port is interpreted. \n For the purpose of status, + an attachment is considered successful as long as the + parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them + by Route kind, namespace, or hostname. If 1 of 2 Gateway + listeners accept attachment from the referencing Route, + the Route MUST be considered successfully attached. If + no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + \n Support: Extended \n " + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: "SectionName is the name of a section within + the target resource. In the following resources, SectionName + is interpreted as the following: \n * Gateway: Listener + Name. When both Port (experimental) and SectionName are + specified, the name and port of the selected listener + must match both specified values. \n Implementations MAY + choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName + is interpreted. \n When unspecified (empty string), this + will reference the entire resource. For the purpose of + status, an attachment is considered successful if at least + one section in the parent resource accepts it. For example, + Gateway listeners can restrict which Routes can attach + to them by Route kind, namespace, or hostname. If 1 of + 2 Gateway listeners accept attachment from the referencing + Route, the Route MUST be considered successfully attached. + If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + required: + - parents + type: object + required: + - spec + type: object + served: false + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.hostnames + name: Hostnames + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: HTTPRoute provides a way to route HTTP requests. This includes + the capability to match requests by hostname, path, header, or query param. + Filters can be used to specify additional processing steps. Backends specify + where matching requests should be routed. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of HTTPRoute. + properties: + hostnames: + description: "Hostnames defines a set of hostname that should match + against the HTTP Host header to select a HTTPRoute used to process + the request. Implementations MUST ignore any port value specified + in the HTTP Host header while performing a match and (absent of + any applicable header modification configuration) MUST forward this + header unmodified to the backend. \n Valid values for Hostnames + are determined by RFC 1123 definition of a hostname with 2 notable + exceptions: \n 1. IPs are not allowed. 2. A hostname may be prefixed + with a wildcard label (`*.`). The wildcard label must appear by + itself as the first label. \n If a hostname is specified by both + the Listener and HTTPRoute, there must be at least one intersecting + hostname for the HTTPRoute to be attached to the Listener. For example: + \n * A Listener with `test.example.com` as the hostname matches + HTTPRoutes that have either not specified any hostnames, or have + specified at least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches HTTPRoutes + that have either not specified any hostnames or have specified at + least one hostname that matches the Listener hostname. For example, + `*.example.com`, `test.example.com`, and `foo.test.example.com` + would all match. On the other hand, `example.com` and `test.example.net` + would not match. \n Hostnames that are prefixed with a wildcard + label (`*.`) are interpreted as a suffix match. That means that + a match for `*.example.com` would match both `test.example.com`, + and `foo.test.example.com`, but not `example.com`. \n If both the + Listener and HTTPRoute have specified hostnames, any HTTPRoute hostnames + that do not match the Listener hostname MUST be ignored. For example, + if a Listener specified `*.example.com`, and the HTTPRoute specified + `test.example.com` and `test.example.net`, `test.example.net` must + not be considered for a match. \n If both the Listener and HTTPRoute + have specified hostnames, and none match with the criteria above, + then the HTTPRoute is not accepted. The implementation must raise + an 'Accepted' Condition with a status of `False` in the corresponding + RouteParentStatus. \n In the event that multiple HTTPRoutes specify + intersecting hostnames (e.g. overlapping wildcard matching and exact + matching hostnames), precedence must be given to rules from the + HTTPRoute with the largest number of: \n * Characters in a matching + non-wildcard hostname. * Characters in a matching hostname. \n If + ties exist across multiple Routes, the matching precedence rules + for HTTPRouteMatches takes over. \n Support: Core" + items: + description: "Hostname is the fully qualified domain name of a network + host. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname + may be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n Hostname can be \"precise\" + which is a domain name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", which is a domain + name prefixed with a single wildcard label (e.g. `*.example.com`). + \n Note that as per RFC1035 and RFC1123, a *label* must consist + of lower case alphanumeric characters or '-', and must start and + end with an alphanumeric character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + parentRefs: + description: "ParentRefs references the resources (usually Gateways) + that a Route wants to be attached to. Note that the referenced parent + resource needs to allow this for the attachment to be complete. + For Gateways, that means the Gateway needs to allow attachment from + Routes of this kind and namespace. \n The only kind of parent resource + with \"Core\" support is Gateway. This API may be extended in the + future to support additional kinds of parent resources such as one + of the route kinds. \n It is invalid to reference an identical parent + more than once. It is valid to reference multiple distinct sections + within the same parent resource, such as 2 Listeners within a Gateway. + \n It is possible to separately reference multiple distinct objects + that may be collapsed by an implementation. For example, some implementations + may choose to merge compatible Gateway Listeners together. If that + is the case, the list of routes attached to those resources should + also be merged. \n Note that for ParentRefs that cross namespace + boundaries, there are specific rules. Cross-namespace references + are only valid if they are explicitly allowed by something in the + namespace they are referring to. For example, Gateway has the AllowedRoutes + field, and ReferenceGrant provides a generic way to enable any other + kind of cross-namespace reference." + items: + description: "ParentReference identifies an API object (usually + a Gateway) that can be considered a parent of this resource (usually + a route). The only kind of parent resource with \"Core\" support + is Gateway. This API may be extended in the future to support + additional kinds of parent resources, such as HTTPRoute. \n The + API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid." + properties: + group: + default: gateway.networking.k8s.io + description: "Group is the group of the referent. When unspecified, + \"gateway.networking.k8s.io\" is inferred. To set the core + API group (such as for a \"Service\" kind referent), Group + must be explicitly set to \"\" (empty string). \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: Core + (Gateway) \n Support: Implementation-specific (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. When + unspecified, this refers to the local namespace of the Route. + \n Note that there are specific rules for ParentRefs which + cross namespace boundaries. Cross-namespace references are + only valid if they are explicitly allowed by something in + the namespace they are referring to. For example: Gateway + has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: "Port is the network port this Route targets. It + can be interpreted differently based on the type of parent + resource. \n When the parent resource is a Gateway, this targets + all listeners listening on the specified port that also support + this kind of Route(and select this Route). It's not recommended + to set `Port` unless the networking behaviors specified in + a Route must apply to a specific port as opposed to a listener(s) + whose port(s) may be changed. When both Port and SectionName + are specified, the name and port of the selected listener + must match both specified values. \n Implementations MAY choose + to support other parent resources. Implementations supporting + other types of parent resources MUST clearly document how/if + Port is interpreted. \n For the purpose of status, an attachment + is considered successful as long as the parent resource accepts + it partially. For example, Gateway listeners can restrict + which Routes can attach to them by Route kind, namespace, + or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this + Route, the Route MUST be considered detached from the Gateway. + \n Support: Extended \n " + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: "SectionName is the name of a section within the + target resource. In the following resources, SectionName is + interpreted as the following: \n * Gateway: Listener Name. + When both Port (experimental) and SectionName are specified, + the name and port of the selected listener must match both + specified values. \n Implementations MAY choose to support + attaching Routes to other resources. If that is the case, + they MUST clearly document how SectionName is interpreted. + \n When unspecified (empty string), this will reference the + entire resource. For the purpose of status, an attachment + is considered successful if at least one section in the parent + resource accepts it. For example, Gateway listeners can restrict + which Routes can attach to them by Route kind, namespace, + or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this + Route, the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + rules: + default: + - matches: + - path: + type: PathPrefix + value: / + description: Rules are a list of HTTP matchers, filters and actions. + items: + description: HTTPRouteRule defines semantics for matching an HTTP + request based on conditions (matches), processing it (filters), + and forwarding the request to an API object (backendRefs). + properties: + backendRefs: + description: "BackendRefs defines the backend(s) where matching + requests should be sent. \n Failure behavior here depends + on how many BackendRefs are specified and how many are invalid. + \n If *all* entries in BackendRefs are invalid, and there + are also no filters specified in this route rule, *all* traffic + which matches this rule MUST receive a 500 status code. \n + See the HTTPBackendRef definition for the rules about what + makes a single HTTPBackendRef invalid. \n When a HTTPBackendRef + is invalid, 500 status codes MUST be returned for requests + that would have otherwise been routed to an invalid backend. + If multiple backends are specified, and some are invalid, + the proportion of requests that would otherwise have been + routed to an invalid backend MUST receive a 500 status code. + \n For example, if two backends are specified with equal weights, + and one is invalid, 50 percent of traffic must receive a 500. + Implementations may choose how that 50 percent is determined. + \n Support: Core for Kubernetes Service \n Support: Extended + for Kubernetes ServiceImport \n Support: Implementation-specific + for any other resource \n Support for weight: Core" + items: + description: HTTPBackendRef defines how a HTTPRoute should + forward an HTTP request. + properties: + filters: + description: "Filters defined at this level should be + executed if and only if the request is being forwarded + to the backend defined here. \n Support: Implementation-specific + (For broader support of filters, use the Filters field + in HTTPRouteRule.)" + items: + description: HTTPRouteFilter defines processing steps + that must be completed during the request or response + lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway + implementations. Some examples include request or + response modification, implementing authentication + strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type + of the filter. + properties: + extensionRef: + description: "ExtensionRef is an optional, implementation-specific + extension to the \"filter\" behavior. For example, + resource \"myroutefilter\" in group \"networking.example.net\"). + ExtensionRef MUST NOT be used for core and extended + filters. \n Support: Implementation-specific" + properties: + group: + description: Group is the group of the referent. + For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API + group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For + example "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema + for a filter that modifies request headers. \n + Support: Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: "RequestMirror defines a schema for + a filter that mirrors requests. Requests are sent + to the specified destination, but responses from + that destination are ignored. \n Support: Extended" + properties: + backendRef: + description: "BackendRef references a resource + where mirrored requests are sent. \n If the + referent cannot be found, this BackendRef + is invalid and must be dropped from the Gateway. + The controller must ensure the \"ResolvedRefs\" + condition on the Route status is set to `status: + False` and not configure this backend in the + underlying implementation. \n If there is + a cross-namespace reference to an *existing* + object that is not allowed by a ReferenceGrant, + the controller must ensure the \"ResolvedRefs\" + \ condition on the Route is set to `status: + False`, with the \"RefNotPermitted\" reason + and not configure this backend in the underlying + implementation. \n In either error case, the + Message of the `ResolvedRefs` Condition should + be used to provide more detail about the problem. + \n Support: Extended for Kubernetes Service + \n Support: Implementation-specific for any + other resource" + properties: + group: + default: "" + description: Group is the group of the referent. + For example, "gateway.networking.k8s.io". + When unspecified or empty string, core + API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: "Kind is the Kubernetes resource + kind of the referent. For example \"Service\". + \n Defaults to \"Service\" when not specified. + \n ExternalName services can refer to + CNAME DNS records that may live outside + of the cluster and as such are difficult + to reason about in terms of conformance. + They also may not be safe to forward to + (see CVE-2021-25740 for more information). + Implementations SHOULD NOT support ExternalName + Services. \n Support: Core (Services with + a type other than ExternalName) \n Support: + Implementation-specific (Services with + type ExternalName)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace + of the backend. When unspecified, the + local namespace is inferred. \n Note that + when a namespace different than the local + namespace is specified, a ReferenceGrant + object is required in the referent namespace + to allow that namespace's owner to accept + the reference. See the ReferenceGrant + documentation for details. \n Support: + Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: Port specifies the destination + port number to use for this resource. + Port is required when the referent is + a Kubernetes Service. In this case, the + port number is the service port number, + not the target port. For other resources, + destination port might be derived from + the referent resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + required: + - backendRef + type: object + requestRedirect: + description: "RequestRedirect defines a schema for + a filter that responds to the request with an + HTTP redirection. \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be + used in the value of the `Location` header + in the response. When empty, the hostname + in the `Host` header of the request is used. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to + modify the path of the incoming request. The + modified path is then used to construct the + `Location` header. When empty, the request + path is used as-is. \n Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in + the value of the `Location` header in the + response. \n If no port is specified, the + redirect port MUST be derived using the following + rules: \n * If redirect scheme is not-empty, + the redirect port MUST be the well-known port + associated with the redirect scheme. Specifically + \"http\" to port 80 and \"https\" to port + 443. If the redirect scheme does not have + a well-known port, the listener port of the + Gateway SHOULD be used. * If redirect scheme + is empty, the redirect port MUST be the Gateway + Listener port. \n Implementations SHOULD NOT + add the port number in the 'Location' header + in the following cases: \n * A Location header + that will use HTTP (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 80. * A Location header that + will use HTTPS (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 443. \n Support: Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used + in the value of the `Location` header in the + response. When empty, the scheme of the request + is used. \n Scheme redirects can affect the + port of the redirect, for more information, + refer to the documentation for the port field + of this filter. \n Note that values may be + added to this enum, implementations must ensure + that unknown values will not cause a crash. + \n Unknown values here must result in the + implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`. \n Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status + code to be used in response. \n Note that + values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result + in the implementation setting the Accepted + Condition for the Route to `status: False`, + with a Reason of `UnsupportedValue`. \n Support: + Core" + enum: + - 301 + - 302 + type: integer + type: object + responseHeaderModifier: + description: "ResponseHeaderModifier defines a schema + for a filter that modifies response headers. \n + Support: Extended" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: "Type identifies the type of filter + to apply. As with other API fields, types are + classified into three conformance levels: \n - + Core: Filter types and their corresponding configuration + defined by \"Support: Core\" in this package, + e.g. \"RequestHeaderModifier\". All implementations + must support core filters. \n - Extended: Filter + types and their corresponding configuration defined + by \"Support: Extended\" in this package, e.g. + \"RequestMirror\". Implementers are encouraged + to support extended filters. \n - Implementation-specific: + Filters that are defined and supported by specific + vendors. In the future, filters showing convergence + in behavior across multiple implementations will + be considered for inclusion in extended or core + conformance levels. Filter-specific configuration + for such filters is specified using the ExtensionRef + field. `Type` should be set to \"ExtensionRef\" + for custom filters. \n Implementers are encouraged + to define custom implementation types to extend + the core API with implementation-specific behavior. + \n If a reference to a custom filter type cannot + be resolved, the filter MUST NOT be skipped. Instead, + requests that would have been processed by that + filter MUST receive a HTTP error response. \n + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result in + the implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestMirror + - RequestRedirect + - URLRewrite + - ExtensionRef + type: string + urlRewrite: + description: "URLRewrite defines a schema for a + filter that modifies a request during forwarding. + \n Support: Extended" + properties: + hostname: + description: "Hostname is the value to be used + to replace the Host header value during forwarding. + \n Support: Extended" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines a path rewrite. \n + Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + type: object + required: + - type + type: object + maxItems: 16 + type: array + group: + default: "" + description: Group is the group of the referent. For example, + "gateway.networking.k8s.io". When unspecified or empty + string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: "Kind is the Kubernetes resource kind of + the referent. For example \"Service\". \n Defaults to + \"Service\" when not specified. \n ExternalName services + can refer to CNAME DNS records that may live outside + of the cluster and as such are difficult to reason about + in terms of conformance. They also may not be safe to + forward to (see CVE-2021-25740 for more information). + Implementations SHOULD NOT support ExternalName Services. + \n Support: Core (Services with a type other than ExternalName) + \n Support: Implementation-specific (Services with type + ExternalName)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the backend. + When unspecified, the local namespace is inferred. \n + Note that when a namespace different than the local + namespace is specified, a ReferenceGrant object is required + in the referent namespace to allow that namespace's + owner to accept the reference. See the ReferenceGrant + documentation for details. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: Port specifies the destination port number + to use for this resource. Port is required when the + referent is a Kubernetes Service. In this case, the + port number is the service port number, not the target + port. For other resources, destination port might be + derived from the referent resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: "Weight specifies the proportion of requests + forwarded to the referenced backend. This is computed + as weight/(sum of all weights in this BackendRefs list). + For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision + an implementation supports. Weight is not a percentage + and the sum of weights does not need to equal 100. \n + If only one backend is specified and it has a weight + greater than 0, 100% of the traffic is forwarded to + that backend. If weight is set to 0, no traffic should + be forwarded for this entry. If unspecified, weight + defaults to 1. \n Support for this field varies based + on the context where used." + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + maxItems: 16 + type: array + filters: + description: "Filters define the filters that are applied to + requests that match this rule. \n The effects of ordering + of multiple behaviors are currently unspecified. This can + change in the future based on feedback during the alpha stage. + \n Conformance-levels at this level are defined based on the + type of filter: \n - ALL core filters MUST be supported by + all implementations. - Implementers are encouraged to support + extended filters. - Implementation-specific custom filters + have no API guarantees across implementations. \n Specifying + a core filter multiple times has unspecified or implementation-specific + conformance. \n All filters are expected to be compatible + with each other except for the URLRewrite and RequestRedirect + filters, which may not be combined. If an implementation can + not support other combinations of filters, they must clearly + document that limitation. In all cases where incompatible + or unsupported filters are specified, implementations MUST + add a warning condition to status. \n Support: Core" + items: + description: HTTPRouteFilter defines processing steps that + must be completed during the request or response lifecycle. + HTTPRouteFilters are meant as an extension point to express + processing that may be done in Gateway implementations. + Some examples include request or response modification, + implementing authentication strategies, rate-limiting, and + traffic shaping. API guarantee/conformance is defined based + on the type of the filter. + properties: + extensionRef: + description: "ExtensionRef is an optional, implementation-specific + extension to the \"filter\" behavior. For example, + resource \"myroutefilter\" in group \"networking.example.net\"). + ExtensionRef MUST NOT be used for core and extended + filters. \n Support: Implementation-specific" + properties: + group: + description: Group is the group of the referent. For + example, "gateway.networking.k8s.io". When unspecified + or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For example + "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema for + a filter that modifies request headers. \n Support: + Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It appends + to any existing values associated with the header + name. \n Input: GET /foo HTTP/1.1 my-header: foo + \n Config: add: - name: \"my-header\" value: \"bar,baz\" + \n Output: GET /foo HTTP/1.1 my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from the + HTTP request before the action. The value of Remove + is a list of HTTP header names. Note that the header + names are case-insensitive (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo my-header2: + bar my-header3: baz \n Config: remove: [\"my-header1\", + \"my-header3\"] \n Output: GET /foo HTTP/1.1 my-header2: + bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with the + given header (name, value) before the action. \n + Input: GET /foo HTTP/1.1 my-header: foo \n Config: + set: - name: \"my-header\" value: \"bar\" \n Output: + GET /foo HTTP/1.1 my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: "RequestMirror defines a schema for a filter + that mirrors requests. Requests are sent to the specified + destination, but responses from that destination are + ignored. \n Support: Extended" + properties: + backendRef: + description: "BackendRef references a resource where + mirrored requests are sent. \n If the referent cannot + be found, this BackendRef is invalid and must be + dropped from the Gateway. The controller must ensure + the \"ResolvedRefs\" condition on the Route status + is set to `status: False` and not configure this + backend in the underlying implementation. \n If + there is a cross-namespace reference to an *existing* + object that is not allowed by a ReferenceGrant, + the controller must ensure the \"ResolvedRefs\" + \ condition on the Route is set to `status: False`, + with the \"RefNotPermitted\" reason and not configure + this backend in the underlying implementation. \n + In either error case, the Message of the `ResolvedRefs` + Condition should be used to provide more detail + about the problem. \n Support: Extended for Kubernetes + Service \n Support: Implementation-specific for + any other resource" + properties: + group: + default: "" + description: Group is the group of the referent. + For example, "gateway.networking.k8s.io". When + unspecified or empty string, core API group + is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: "Kind is the Kubernetes resource + kind of the referent. For example \"Service\". + \n Defaults to \"Service\" when not specified. + \n ExternalName services can refer to CNAME + DNS records that may live outside of the cluster + and as such are difficult to reason about in + terms of conformance. They also may not be safe + to forward to (see CVE-2021-25740 for more information). + Implementations SHOULD NOT support ExternalName + Services. \n Support: Core (Services with a + type other than ExternalName) \n Support: Implementation-specific + (Services with type ExternalName)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the + backend. When unspecified, the local namespace + is inferred. \n Note that when a namespace different + than the local namespace is specified, a ReferenceGrant + object is required in the referent namespace + to allow that namespace's owner to accept the + reference. See the ReferenceGrant documentation + for details. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: Port specifies the destination port + number to use for this resource. Port is required + when the referent is a Kubernetes Service. In + this case, the port number is the service port + number, not the target port. For other resources, + destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + required: + - backendRef + type: object + requestRedirect: + description: "RequestRedirect defines a schema for a filter + that responds to the request with an HTTP redirection. + \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be used + in the value of the `Location` header in the response. + When empty, the hostname in the `Host` header of + the request is used. \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to modify + the path of the incoming request. The modified path + is then used to construct the `Location` header. + When empty, the request path is used as-is. \n Support: + Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the value + with which to replace the full path of a request + during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies the + value with which to replace the prefix match + of a request during a rewrite or redirect. For + example, a request to \"/foo/bar\" with a prefix + match of \"/foo\" and a ReplacePrefixMatch of + \"/xyz\" would be modified to \"/xyz/bar\". + \n Note that this matches the behavior of the + PathPrefix match type. This matches full path + elements. A path element refers to the list + of labels in the path split by the `/` separator. + When specified, a trailing `/` is ignored. For + example, the paths `/abc`, `/abc/`, and `/abc/def` + would all match the prefix `/abc`, but the path + `/abcd` would not. \n Request Path | Prefix + Match | Replace Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | /xyz/ + \ | /xyz/bar /foo/bar | /foo/ | + /xyz | /xyz/bar /foo/bar | /foo/ + \ | /xyz/ | /xyz/bar /foo | + /foo | /xyz | /xyz /foo/ | + /foo | /xyz | /xyz/ /foo/bar + \ | /foo | | /bar + /foo/ | /foo | + | / /foo | /foo | + | / /foo/ | /foo | / | + / /foo | /foo | / | + /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path modifier. + Additional types may be added in a future release + of the API. \n Note that values may be added + to this enum, implementations must ensure that + unknown values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the Route + to `status: False`, with a Reason of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in the value + of the `Location` header in the response. \n If + no port is specified, the redirect port MUST be + derived using the following rules: \n * If redirect + scheme is not-empty, the redirect port MUST be the + well-known port associated with the redirect scheme. + Specifically \"http\" to port 80 and \"https\" to + port 443. If the redirect scheme does not have a + well-known port, the listener port of the Gateway + SHOULD be used. * If redirect scheme is empty, the + redirect port MUST be the Gateway Listener port. + \n Implementations SHOULD NOT add the port number + in the 'Location' header in the following cases: + \n * A Location header that will use HTTP (whether + that is determined via the Listener protocol or + the Scheme field) _and_ use port 80. * A Location + header that will use HTTPS (whether that is determined + via the Listener protocol or the Scheme field) _and_ + use port 443. \n Support: Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used in the + value of the `Location` header in the response. + When empty, the scheme of the request is used. \n + Scheme redirects can affect the port of the redirect, + for more information, refer to the documentation + for the port field of this filter. \n Note that + values may be added to this enum, implementations + must ensure that unknown values will not cause a + crash. \n Unknown values here must result in the + implementation setting the Accepted Condition for + the Route to `status: False`, with a Reason of `UnsupportedValue`. + \n Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status code to + be used in response. \n Note that values may be + added to this enum, implementations must ensure + that unknown values will not cause a crash. \n Unknown + values here must result in the implementation setting + the Accepted Condition for the Route to `status: + False`, with a Reason of `UnsupportedValue`. \n + Support: Core" + enum: + - 301 + - 302 + type: integer + type: object + responseHeaderModifier: + description: "ResponseHeaderModifier defines a schema + for a filter that modifies response headers. \n Support: + Extended" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It appends + to any existing values associated with the header + name. \n Input: GET /foo HTTP/1.1 my-header: foo + \n Config: add: - name: \"my-header\" value: \"bar,baz\" + \n Output: GET /foo HTTP/1.1 my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from the + HTTP request before the action. The value of Remove + is a list of HTTP header names. Note that the header + names are case-insensitive (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo my-header2: + bar my-header3: baz \n Config: remove: [\"my-header1\", + \"my-header3\"] \n Output: GET /foo HTTP/1.1 my-header2: + bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with the + given header (name, value) before the action. \n + Input: GET /foo HTTP/1.1 my-header: foo \n Config: + set: - name: \"my-header\" value: \"bar\" \n Output: + GET /foo HTTP/1.1 my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: "Type identifies the type of filter to apply. + As with other API fields, types are classified into + three conformance levels: \n - Core: Filter types and + their corresponding configuration defined by \"Support: + Core\" in this package, e.g. \"RequestHeaderModifier\". + All implementations must support core filters. \n - + Extended: Filter types and their corresponding configuration + defined by \"Support: Extended\" in this package, e.g. + \"RequestMirror\". Implementers are encouraged to support + extended filters. \n - Implementation-specific: Filters + that are defined and supported by specific vendors. + In the future, filters showing convergence in behavior + across multiple implementations will be considered for + inclusion in extended or core conformance levels. Filter-specific + configuration for such filters is specified using the + ExtensionRef field. `Type` should be set to \"ExtensionRef\" + for custom filters. \n Implementers are encouraged to + define custom implementation types to extend the core + API with implementation-specific behavior. \n If a reference + to a custom filter type cannot be resolved, the filter + MUST NOT be skipped. Instead, requests that would have + been processed by that filter MUST receive a HTTP error + response. \n Note that values may be added to this enum, + implementations must ensure that unknown values will + not cause a crash. \n Unknown values here must result + in the implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason of `UnsupportedValue`." + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestMirror + - RequestRedirect + - URLRewrite + - ExtensionRef + type: string + urlRewrite: + description: "URLRewrite defines a schema for a filter + that modifies a request during forwarding. \n Support: + Extended" + properties: + hostname: + description: "Hostname is the value to be used to + replace the Host header value during forwarding. + \n Support: Extended" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines a path rewrite. \n Support: + Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the value + with which to replace the full path of a request + during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies the + value with which to replace the prefix match + of a request during a rewrite or redirect. For + example, a request to \"/foo/bar\" with a prefix + match of \"/foo\" and a ReplacePrefixMatch of + \"/xyz\" would be modified to \"/xyz/bar\". + \n Note that this matches the behavior of the + PathPrefix match type. This matches full path + elements. A path element refers to the list + of labels in the path split by the `/` separator. + When specified, a trailing `/` is ignored. For + example, the paths `/abc`, `/abc/`, and `/abc/def` + would all match the prefix `/abc`, but the path + `/abcd` would not. \n Request Path | Prefix + Match | Replace Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | /xyz/ + \ | /xyz/bar /foo/bar | /foo/ | + /xyz | /xyz/bar /foo/bar | /foo/ + \ | /xyz/ | /xyz/bar /foo | + /foo | /xyz | /xyz /foo/ | + /foo | /xyz | /xyz/ /foo/bar + \ | /foo | | /bar + /foo/ | /foo | + | / /foo | /foo | + | / /foo/ | /foo | / | + / /foo | /foo | / | + /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path modifier. + Additional types may be added in a future release + of the API. \n Note that values may be added + to this enum, implementations must ensure that + unknown values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the Route + to `status: False`, with a Reason of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + type: object + required: + - type + type: object + maxItems: 16 + type: array + matches: + default: + - path: + type: PathPrefix + value: / + description: "Matches define conditions used for matching the + rule against incoming HTTP requests. Each match is independent, + i.e. this rule will be matched if **any** one of the matches + is satisfied. \n For example, take the following matches configuration: + \n ``` matches: - path: value: \"/foo\" headers: - name: \"version\" + value: \"v2\" - path: value: \"/v2/foo\" ``` \n For a request + to match against this rule, a request must satisfy EITHER + of the two conditions: \n - path prefixed with `/foo` AND + contains the header `version: v2` - path prefix of `/v2/foo` + \n See the documentation for HTTPRouteMatch on how to specify + multiple match conditions that should be ANDed together. \n + If no matches are specified, the default is a prefix path + match on \"/\", which has the effect of matching every HTTP + request. \n Proxy or Load Balancer routing configuration generated + from HTTPRoutes MUST prioritize matches based on the following + criteria, continuing on ties. Across all rules specified on + applicable Routes, precedence must be given to the match having: + \n * \"Exact\" path match. * \"Prefix\" path match with largest + number of characters. * Method match. * Largest number of + header matches. * Largest number of query param matches. \n + Note: The precedence of RegularExpression path matches are + implementation-specific. \n If ties still exist across multiple + Routes, matching precedence MUST be determined in order of + the following criteria, continuing on ties: \n * The oldest + Route based on creation timestamp. * The Route appearing first + in alphabetical order by \"{namespace}/{name}\". \n If ties + still exist within an HTTPRoute, matching precedence MUST + be granted to the FIRST matching rule (in list order) with + a match meeting the above criteria. \n When no rules matching + a request have been successfully attached to the parent a + request is coming from, a HTTP 404 status code MUST be returned." + items: + description: "HTTPRouteMatch defines the predicate used to + match requests to a given action. Multiple match types are + ANDed together, i.e. the match will evaluate to true only + if all conditions are satisfied. \n For example, the match + below will match a HTTP request only if its path starts + with `/foo` AND it contains the `version: v1` header: \n + ``` match: \n path: value: \"/foo\" headers: - name: \"version\" + value \"v1\" \n ```" + properties: + headers: + description: Headers specifies HTTP request header matchers. + Multiple match values are ANDed together, meaning, a + request must match all the specified headers to select + the route. + items: + description: HTTPHeaderMatch describes how to select + a HTTP route by matching HTTP request headers. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case insensitive. + (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent header + names, only the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST be + ignored. Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered equivalent. + \n When a header is repeated in an HTTP request, + it is implementation-specific behavior as to how + this is represented. Generally, proxies should + follow the guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, with special + handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the header. \n Support: Core (Exact) + \n Support: Implementation-specific (RegularExpression) + \n Since RegularExpression HeaderMatchType has + implementation-specific conformance, implementations + can support POSIX, PCRE or any other dialects + of regular expressions. Please read the implementation's + documentation to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP Header to + be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. When + specified, this route will be matched only if the request + has the specified method. \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path matcher. + If this field is not specified, a default prefix match + on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match against + the path Value. \n Support: Core (Exact, PathPrefix) + \n Support: Implementation-specific (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match against. + maxLength: 1024 + type: string + type: object + queryParams: + description: "QueryParams specifies HTTP query parameter + matchers. Multiple match values are ANDed together, + meaning, a request must match all the specified query + parameters to select the route. \n Support: Extended" + items: + description: HTTPQueryParamMatch describes how to select + a HTTP route by matching HTTP query parameters. + properties: + name: + description: "Name is the name of the HTTP query + param to be matched. This must be an exact string + match. (See https://tools.ietf.org/html/rfc7230#section-2.7.3). + \n If multiple entries specify equivalent query + param names, only the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent query param name MUST + be ignored. \n If a query param is repeated in + an HTTP request, the behavior is purposely left + undefined, since different data planes have different + capabilities. However, it is *recommended* that + implementations should match against the first + value of the param if the data plane supports + it, as this behavior is expected in other load + balancing contexts outside of the Gateway API. + \n Users SHOULD NOT route traffic based on repeated + query params to guard themselves against potential + differences in the implementations." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the query parameter. \n Support: + Extended (Exact) \n Support: Implementation-specific + (RegularExpression) \n Since RegularExpression + QueryParamMatchType has Implementation-specific + conformance, implementations can support POSIX, + PCRE or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP query param + to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + maxItems: 8 + type: array + type: object + maxItems: 16 + type: array + type: object + status: + description: Status defines the current state of HTTPRoute. + properties: + parents: + description: "Parents is a list of parent resources (usually Gateways) + that are associated with the route, and the status of the route + with respect to each parent. When this route attaches to a parent, + the controller that manages the parent must add an entry to this + list when the controller first sees the route and should update + the entry as appropriate when the route or gateway is modified. + \n Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this + API can only populate Route status for the Gateways/parent resources + they are responsible for. \n A maximum of 32 Gateways will be represented + in this list. An empty list means the route has not been attached + to any Gateway." + items: + description: RouteParentStatus describes the status of a route with + respect to an associated Parent. + properties: + conditions: + description: "Conditions describes the status of the route with + respect to the Gateway. Note that the route's availability + is also subject to the Gateway's own status conditions and + listener status. \n If the Route's ParentRef specifies an + existing Gateway that supports Routes of this kind AND that + Gateway's controller has sufficient access, then that Gateway's + controller MUST set the \"Accepted\" condition on the Route, + to indicate whether the route has been accepted or rejected + by the Gateway, and why. \n A Route MUST be considered \"Accepted\" + if at least one of the Route's rules is implemented by the + Gateway. \n There are a number of cases where the \"Accepted\" + condition may not be set due to lack of controller visibility, + that includes when: \n * The Route refers to a non-existent + parent. * The Route is of a type that the controller does + not support. * The Route is in a namespace the controller + does not have access to." + items: + description: "Condition contains details for one aspect of + the current state of this API Resource. --- This struct + is intended for direct use as an array at the field path + .status.conditions. For example, \n type FooStatus struct{ + // Represents the observations of a foo's current state. + // Known .status.conditions.type are: \"Available\", \"Progressing\", + and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields + }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should + be when the underlying condition changed. If that is + not known, then using the time when the API field changed + is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, + if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the + current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier + indicating the reason for the condition's last transition. + Producers of specific condition types may define expected + values and meanings for this field, and whether the + values are considered a guaranteed API. The value should + be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across + resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability + to deconflict is important. The regex it matches is + (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: "ControllerName is a domain/path string that indicates + the name of the controller that wrote this status. This corresponds + with the controllerName field on GatewayClass. \n Example: + \"example.net/gateway-controller\". \n The format of this + field is DOMAIN \"/\" PATH, where DOMAIN and PATH are valid + Kubernetes names (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + \n Controllers MUST populate this field when writing status. + Controllers should ensure that entries to status populated + with their ControllerName are cleaned up when they are no + longer necessary." + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: ParentRef corresponds with a ParentRef in the spec + that this RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: "Group is the group of the referent. When unspecified, + \"gateway.networking.k8s.io\" is inferred. To set the + core API group (such as for a \"Service\" kind referent), + Group must be explicitly set to \"\" (empty string). \n + Support: Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: + Core (Gateway) \n Support: Implementation-specific (Other + Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. + When unspecified, this refers to the local namespace of + the Route. \n Note that there are specific rules for ParentRefs + which cross namespace boundaries. Cross-namespace references + are only valid if they are explicitly allowed by something + in the namespace they are referring to. For example: Gateway + has the AllowedRoutes field, and ReferenceGrant provides + a generic way to enable any other kind of cross-namespace + reference. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: "Port is the network port this Route targets. + It can be interpreted differently based on the type of + parent resource. \n When the parent resource is a Gateway, + this targets all listeners listening on the specified + port that also support this kind of Route(and select this + Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to + a specific port as opposed to a listener(s) whose port(s) + may be changed. When both Port and SectionName are specified, + the name and port of the selected listener must match + both specified values. \n Implementations MAY choose to + support other parent resources. Implementations supporting + other types of parent resources MUST clearly document + how/if Port is interpreted. \n For the purpose of status, + an attachment is considered successful as long as the + parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them + by Route kind, namespace, or hostname. If 1 of 2 Gateway + listeners accept attachment from the referencing Route, + the Route MUST be considered successfully attached. If + no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + \n Support: Extended \n " + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: "SectionName is the name of a section within + the target resource. In the following resources, SectionName + is interpreted as the following: \n * Gateway: Listener + Name. When both Port (experimental) and SectionName are + specified, the name and port of the selected listener + must match both specified values. \n Implementations MAY + choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName + is interpreted. \n When unspecified (empty string), this + will reference the entire resource. For the purpose of + status, an attachment is considered successful if at least + one section in the parent resource accepts it. For example, + Gateway listeners can restrict which Routes can attach + to them by Route kind, namespace, or hostname. If 1 of + 2 Gateway listeners accept attachment from the referencing + Route, the Route MUST be considered successfully attached. + If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null +{{- end }} diff --git a/linkerd/charts/linkerd-crds/templates/policy/authorization-policy.yaml b/linkerd/charts/linkerd-crds/templates/policy/authorization-policy.yaml new file mode 100644 index 0000000..7d86520 --- /dev/null +++ b/linkerd/charts/linkerd-crds/templates/policy/authorization-policy.yaml @@ -0,0 +1,99 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: authorizationpolicies.policy.linkerd.io + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + linkerd.io/control-plane-ns: {{.Release.Namespace}} +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: AuthorizationPolicy + plural: authorizationpolicies + singular: authorizationpolicy + shortNames: [authzpolicy] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied server + resources. + type: object + required: [targetRef, requiredAuthenticationRefs] + properties: + targetRef: + description: >- + TargetRef references a resource to which the authorization + policy applies. + type: object + required: [kind, name] + # Modified from the gateway API. + # Copyright 2020 The Kubernetes Authors + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + requiredAuthenticationRefs: + description: >- + RequiredAuthenticationRefs enumerates a set of required + authentications. ALL authentications must be satisfied for + the authorization to apply. If any of the referred objects + cannot be found, the authorization will be ignored. + type: array + items: + type: object + required: [kind, name] + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string diff --git a/linkerd/charts/linkerd-crds/templates/policy/httproute.yaml b/linkerd/charts/linkerd-crds/templates/policy/httproute.yaml new file mode 100644 index 0000000..9aaaefc --- /dev/null +++ b/linkerd/charts/linkerd-crds/templates/policy/httproute.yaml @@ -0,0 +1,5216 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: httproutes.policy.linkerd.io + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + linkerd.io/control-plane-ns: {{.Release.Namespace}} +spec: + group: policy.linkerd.io + names: + kind: HTTPRoute + listKind: HTTPRouteList + plural: httproutes + singular: httproute + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.hostnames + name: Hostnames + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: HTTPRoute provides a way to route HTTP requests. This includes + the capability to match requests by hostname, path, header, or query param. + Filters can be used to specify additional processing steps. Backends specify + where matching requests should be routed. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of HTTPRoute. + properties: + hostnames: + description: "Hostnames defines a set of hostname that should match + against the HTTP Host header to select a HTTPRoute to process the + request. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname may + be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n If a hostname is specified + by both the Listener and HTTPRoute, there must be at least one intersecting + hostname for the HTTPRoute to be attached to the Listener. For example: + \n * A Listener with `test.example.com` as the hostname matches + HTTPRoutes that have either not specified any hostnames, or have + specified at least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches HTTPRoutes + \ that have either not specified any hostnames or have specified + at least one hostname that matches the Listener hostname. For + example, `*.example.com`, `test.example.com`, and `foo.test.example.com` + would all match. On the other hand, `example.com` and `test.example.net` + would not match. \n Hostnames that are prefixed with a wildcard + label (`*.`) are interpreted as a suffix match. That means that + a match for `*.example.com` would match both `test.example.com`, + and `foo.test.example.com`, but not `example.com`. \n If both the + Listener and HTTPRoute have specified hostnames, any HTTPRoute hostnames + that do not match the Listener hostname MUST be ignored. For example, + if a Listener specified `*.example.com`, and the HTTPRoute specified + `test.example.com` and `test.example.net`, `test.example.net` must + not be considered for a match. \n If both the Listener and HTTPRoute + have specified hostnames, and none match with the criteria above, + then the HTTPRoute is not accepted. The implementation must raise + an 'Accepted' Condition with a status of `False` in the corresponding + RouteParentStatus. \n Support: Core" + items: + description: "Hostname is the fully qualified domain name of a network + host. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname + may be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n Hostname can be \"precise\" + which is a domain name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", which is a domain + name prefixed with a single wildcard label (e.g. `*.example.com`). + \n Note that as per RFC1035 and RFC1123, a *label* must consist + of lower case alphanumeric characters or '-', and must start and + end with an alphanumeric character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + parentRefs: + description: "ParentRefs references the resources (usually Gateways) + that a Route wants to be attached to. Note that the referenced parent + resource needs to allow this for the attachment to be complete. + For Gateways, that means the Gateway needs to allow attachment from + Routes of this kind and namespace. \n The only kind of parent resource + with \"Core\" support is Gateway. This API may be extended in the + future to support additional kinds of parent resources such as one + of the route kinds. \n It is invalid to reference an identical parent + more than once. It is valid to reference multiple distinct sections + within the same parent resource, such as 2 Listeners within a Gateway. + \n It is possible to separately reference multiple distinct objects + that may be collapsed by an implementation. For example, some implementations + may choose to merge compatible Gateway Listeners together. If that + is the case, the list of routes attached to those resources should + also be merged." + items: + description: "ParentReference identifies an API object (usually + a Gateway) that can be considered a parent of this resource (usually + a route). The only kind of parent resource with \"Core\" support + is Gateway. This API may be extended in the future to support + additional kinds of parent resources, such as HTTPRoute. \n The + API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid." + properties: + group: + default: policy.linkerd.io + description: "Group is the group of the referent. \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: Core + (Gateway) Support: Custom (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. When + unspecified (or empty string), this refers to the local namespace + of the Route. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: "port" + type: integer + sectionName: + description: "SectionName is the name of a section within the + target resource. In the following resources, SectionName is + interpreted as the following: \n * Gateway: Listener Name. + When both Port (experimental) and SectionName are specified, + the name and port of the selected listener must match both + specified values. \n Implementations MAY choose to support + attaching Routes to other resources. If that is the case, + they MUST clearly document how SectionName is interpreted. + \n When unspecified (empty string), this will reference the + entire resource. For the purpose of status, an attachment + is considered successful if at least one section in the parent + resource accepts it. For example, Gateway listeners can restrict + which Routes can attach to them by Route kind, namespace, + or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this + Route, the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + rules: + default: + - matches: + - path: + type: PathPrefix + value: / + description: Rules are a list of HTTP matchers, filters and actions. + items: + description: HTTPRouteRule defines semantics for matching an HTTP + request based on conditions (matches) and processing it (filters). + properties: + backendRefs: + type: array + items: + type: object + properties: + name: + type: string + port: + type: integer + namespace: + type: string + default: "default" + filters: + description: "Filters defined at this level should be + executed if and only if the request is being forwarded + to the backend defined here. \n Support: Implementation-specific + (For broader support of filters, use the Filters field + in HTTPRouteRule.)" + items: + description: HTTPRouteFilter defines processing steps + that must be completed during the request or response + lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway + implementations. Some examples include request or + response modification, implementing authentication + strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type + of the filter. + properties: + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema + for a filter that modifies request headers. \n + Support: Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestRedirect: + description: "RequestRedirect defines a schema for + a filter that responds to the request with an + HTTP redirection. \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be + used in the value of the `Location` header + in the response. When empty, the hostname + in the `Host` header of the request is used. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to + modify the path of the incoming request. The + modified path is then used to construct the + `Location` header. When empty, the request + path is used as-is. \n Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in + the value of the `Location` header in the + response. \n If no port is specified, the + redirect port MUST be derived using the following + rules: \n * If redirect scheme is not-empty, + the redirect port MUST be the well-known port + associated with the redirect scheme. Specifically + \"http\" to port 80 and \"https\" to port + 443. If the redirect scheme does not have + a well-known port, the listener port of the + Gateway SHOULD be used. * If redirect scheme + is empty, the redirect port MUST be the Gateway + Listener port. \n Implementations SHOULD NOT + add the port number in the 'Location' header + in the following cases: \n * A Location header + that will use HTTP (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 80. * A Location header that + will use HTTPS (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 443. \n Support: Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used + in the value of the `Location` header in the + response. When empty, the scheme of the request + is used. \n Scheme redirects can affect the + port of the redirect, for more information, + refer to the documentation for the port field + of this filter. \n Note that values may be + added to this enum, implementations must ensure + that unknown values will not cause a crash. + \n Unknown values here must result in the + implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`. \n Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status + code to be used in response. \n Note that + values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result + in the implementation setting the Accepted + Condition for the Route to `status: False`, + with a Reason of `UnsupportedValue`. \n Support: + Core" + enum: + - 301 + - 302 + type: integer + type: object + responseHeaderModifier: + description: "ResponseHeaderModifier defines a schema + for a filter that modifies response headers. \n + Support: Extended" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: "Type identifies the type of filter + to apply. As with other API fields, types are + classified into three conformance levels: \n - + Core: Filter types and their corresponding configuration + defined by \"Support: Core\" in this package, + e.g. \"RequestHeaderModifier\". All implementations + must support core filters. \n - Extended: Filter + types and their corresponding configuration defined + by \"Support: Extended\" in this package, e.g. + \"RequestMirror\". Implementers are encouraged + to support extended filters. \n - Implementation-specific: + Filters that are defined and supported by specific + vendors. In the future, filters showing convergence + in behavior across multiple implementations will + be considered for inclusion in extended or core + conformance levels. Filter-specific configuration + for such filters is specified using the ExtensionRef + field. `Type` should be set to \"ExtensionRef\" + for custom filters. \n Implementers are encouraged + to define custom implementation types to extend + the core API with implementation-specific behavior. + \n If a reference to a custom filter type cannot + be resolved, the filter MUST NOT be skipped. Instead, + requests that would have been processed by that + filter MUST receive a HTTP error response. \n + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result in + the implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestRedirect + type: string + required: + - type + type: object + maxItems: 16 + type: array + filters: + description: "Filters define the filters that are applied to + requests that match this rule. \n The effects of ordering + of multiple behaviors are currently unspecified. This can + change in the future based on feedback during the alpha stage. + \n Conformance-levels at this level are defined based on the + type of filter: \n - ALL core filters MUST be supported by + all implementations. - Implementers are encouraged to support + extended filters. - Implementation-specific custom filters + have no API guarantees across implementations. \n Specifying + a core filter multiple times has unspecified or custom conformance. + \n All filters are expected to be compatible with each other + except for the URLRewrite and RequestRedirect filters, which + may not be combined. If an implementation can not support + other combinations of filters, they must clearly document + that limitation. In all cases where incompatible or unsupported + filters are specified, implementations MUST add a warning + condition to status. \n Support: Core" + items: + description: HTTPRouteFilter defines processing steps that + must be completed during the request or response lifecycle. + HTTPRouteFilters are meant as an extension point to express + processing that may be done in Gateway implementations. + Some examples include request or response modification, + implementing authentication strategies, rate-limiting, and + traffic shaping. API guarantee/conformance is defined based + on the type of the filter. + properties: + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema for + a filter that modifies request headers. \n Support: + Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It appends + to any existing values associated with the header + name. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: add: - name: \"my-header\" value: + \"bar\" \n Output: GET /foo HTTP/1.1 my-header: + foo my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from the + HTTP request before the action. The value of Remove + is a list of HTTP header names. Note that the header + names are case-insensitive (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + \ my-header2: bar my-header3: baz \n Config: + \ remove: [\"my-header1\", \"my-header3\"] \n Output: + \ GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with the + given header (name, value) before the action. \n + Input: GET /foo HTTP/1.1 my-header: foo \n Config: + \ set: - name: \"my-header\" value: \"bar\" + \n Output: GET /foo HTTP/1.1 my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestRedirect: + description: "RequestRedirect defines a schema for a filter + that responds to the request with an HTTP redirection. + \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be used + in the value of the `Location` header in the response. + When empty, the hostname of the request is used. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to + modify the path of the incoming request. The + modified path is then used to construct the + `Location` header. When empty, the request + path is used as-is. \n Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in the value + of the `Location` header in the response. When empty, + port (if specified) of the request is used. \n Support: + Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used in the + value of the `Location` header in the response. + When empty, the scheme of the request is used. \n + Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status code to + be used in response. \n Support: Core" + enum: + - 301 + - 302 + type: integer + type: object + type: + description: "Type identifies the type of filter to apply. + As with other API fields, types are classified into + three conformance levels: \n - Core: Filter types and + their corresponding configuration defined by \"Support: + Core\" in this package, e.g. \"RequestHeaderModifier\". + All implementations must support core filters. \n\n " + enum: + - RequestHeaderModifier + - RequestRedirect + type: string + required: + - type + type: object + maxItems: 16 + type: array + matches: + default: + - path: + type: PathPrefix + value: / + description: "Matches define conditions used for matching the + rule against incoming HTTP requests. Each match is independent, + i.e. this rule will be matched if **any** one of the matches + is satisfied. \n For example, take the following matches configuration: + \n ``` matches: - path: value: \"/foo\" headers: - + name: \"version\" value: \"v2\" - path: value: \"/v2/foo\" + ``` \n For a request to match against this rule, a request + must satisfy EITHER of the two conditions: \n - path prefixed + with `/foo` AND contains the header `version: v2` - path prefix + of `/v2/foo` \n See the documentation for HTTPRouteMatch on + how to specify multiple match conditions that should be ANDed + together. \n If no matches are specified, the default is a + prefix path match on \"/\", which has the effect of matching + every HTTP request. \n Proxy or Load Balancer routing configuration + generated from HTTPRoutes MUST prioritize rules based on the + following criteria, continuing on ties. Precedence must be + given to the the Rule with the largest number of: \n * Characters + in a matching non-wildcard hostname. * Characters in a matching + hostname. * Characters in a matching path. * Header matches. + * Query param matches. \n If ties still exist across multiple + Routes, matching precedence MUST be determined in order of + the following criteria, continuing on ties: \n * The oldest + Route based on creation timestamp. * The Route appearing first + in alphabetical order by \"{namespace}/{name}\". \n If ties + still exist within the Route that has been given precedence, + matching precedence MUST be granted to the first matching + rule meeting the above criteria. \n When no rules matching + a request have been successfully attached to the parent a + request is coming from, a HTTP 404 status code MUST be returned." + items: + description: "HTTPRouteMatch defines the predicate used to + match requests to a given action. Multiple match types are + ANDed together, i.e. the match will evaluate to true only + if all conditions are satisfied. \n For example, the match + below will match a HTTP request only if its path starts + with `/foo` AND it contains the `version: v1` header: \n + ``` match: path: value: \"/foo\" headers: - name: + \"version\" value \"v1\" ```" + properties: + headers: + description: Headers specifies HTTP request header matchers. + Multiple match values are ANDed together, meaning, a + request must match all the specified headers to select + the route. + items: + description: HTTPHeaderMatch describes how to select + a HTTP route by matching HTTP request headers. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case insensitive. + (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent header + names, only the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST be + ignored. Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered equivalent. + \n When a header is repeated in an HTTP request, + it is implementation-specific behavior as to how + this is represented. Generally, proxies should + follow the guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, with special + handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the header. \n Support: Core (Exact) + \n Support: Custom (RegularExpression) \n Since + RegularExpression HeaderMatchType has custom conformance, + implementations can support POSIX, PCRE or any + other dialects of regular expressions. Please + read the implementation's documentation to determine + the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP Header to + be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. When + specified, this route will be matched only if the request + has the specified method. \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path matcher. + If this field is not specified, a default prefix match + on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match against + the path Value. \n Support: Core (Exact, PathPrefix) + \n Support: Custom (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match against. + maxLength: 1024 + type: string + type: object + queryParams: + description: QueryParams specifies HTTP query parameter + matchers. Multiple match values are ANDed together, + meaning, a request must match all the specified query + parameters to select the route. + items: + description: HTTPQueryParamMatch describes how to select + a HTTP route by matching HTTP query parameters. + properties: + name: + description: Name is the name of the HTTP query + param to be matched. This must be an exact string + match. (See https://tools.ietf.org/html/rfc7230#section-2.7.3). + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the query parameter. \n Support: + Extended (Exact) \n Support: Custom (RegularExpression) + \n Since RegularExpression QueryParamMatchType + has custom conformance, implementations can support + POSIX, PCRE or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP query param + to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + maxItems: 8 + type: array + type: object + maxItems: 16 + type: array + type: object + status: + description: Status defines the current state of HTTPRoute. + properties: + parents: + description: "Parents is a list of parent resources (usually Gateways) + that are associated with the route, and the status of the route + with respect to each parent. When this route attaches to a parent, + the controller that manages the parent must add an entry to this + list when the controller first sees the route and should update + the entry as appropriate when the route or gateway is modified. + \n Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this + API can only populate Route status for the Gateways/parent resources + they are responsible for. \n A maximum of 32 Gateways will be represented + in this list. An empty list means the route has not been attached + to any Gateway." + items: + description: RouteParentStatus describes the status of a route with + respect to an associated Parent. + properties: + conditions: + description: "Conditions describes the status of the route with + respect to the Gateway. Note that the route's availability + is also subject to the Gateway's own status conditions and + listener status. \n If the Route's ParentRef specifies an + existing Gateway that supports Routes of this kind AND that + Gateway's controller has sufficient access, then that Gateway's + controller MUST set the \"Accepted\" condition on the Route, + to indicate whether the route has been accepted or rejected + by the Gateway, and why. \n A Route MUST be considered \"Accepted\" + if at least one of the Route's rules is implemented by the + Gateway. \n There are a number of cases where the \"Accepted\" + condition may not be set due to lack of controller visibility, + that includes when: \n * The Route refers to a non-existent + parent. * The Route is of a type that the controller does + not support. * The Route is in a namespace the the controller + does not have access to." + items: + description: "Condition contains details for one aspect of + the current state of this API Resource. --- This struct + is intended for direct use as an array at the field path + .status.conditions. For example, type FooStatus struct{ + \ // Represents the observations of a foo's current state. + \ // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type + \ // +patchStrategy=merge // +listType=map // + +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` + \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should + be when the underlying condition changed. If that is + not known, then using the time when the API field changed + is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, + if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the + current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier + indicating the reason for the condition's last transition. + Producers of specific condition types may define expected + values and meanings for this field, and whether the + values are considered a guaranteed API. The value should + be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across + resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability + to deconflict is important. The regex it matches is + (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: "ControllerName is a domain/path string that indicates + the name of the controller that wrote this status. This corresponds + with the controllerName field on GatewayClass. \n Example: + \"example.net/gateway-controller\". \n The format of this + field is DOMAIN \"/\" PATH, where DOMAIN and PATH are valid + Kubernetes names (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + \n Controllers MUST populate this field when writing status. + Controllers should ensure that entries to status populated + with their ControllerName are cleaned up when they are no + longer necessary." + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: ParentRef corresponds with a ParentRef in the spec + that this RouteParentStatus struct describes the status of. + properties: + group: + default: policy.linkerd.io + description: "Group is the group of the referent. \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: + Core (Gateway) Support: Custom (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. + When unspecified (or empty string), this refers to the + local namespace of the Route. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + sectionName: + description: "SectionName is the name of a section within + the target resource. In the following resources, SectionName + is interpreted as the following: \n * Gateway: Listener + Name. When both Port (experimental) and SectionName are + specified, the name and port of the selected listener + must match both specified values. \n Implementations MAY + choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName + is interpreted. \n When unspecified (empty string), this + will reference the entire resource. For the purpose of + status, an attachment is considered successful if at least + one section in the parent resource accepts it. For example, + Gateway listeners can restrict which Routes can attach + to them by Route kind, namespace, or hostname. If 1 of + 2 Gateway listeners accept attachment from the referencing + Route, the Route MUST be considered successfully attached. + If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.hostnames + name: Hostnames + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: HTTPRoute provides a way to route HTTP requests. This includes + the capability to match requests by hostname, path, header, or query param. + Filters can be used to specify additional processing steps. Backends specify + where matching requests should be routed. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of HTTPRoute. + properties: + hostnames: + description: "Hostnames defines a set of hostname that should match + against the HTTP Host header to select a HTTPRoute to process the + request. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname may + be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n If a hostname is specified + by both the Listener and HTTPRoute, there must be at least one intersecting + hostname for the HTTPRoute to be attached to the Listener. For example: + \n * A Listener with `test.example.com` as the hostname matches + HTTPRoutes that have either not specified any hostnames, or have + specified at least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches HTTPRoutes + \ that have either not specified any hostnames or have specified + at least one hostname that matches the Listener hostname. For + example, `*.example.com`, `test.example.com`, and `foo.test.example.com` + would all match. On the other hand, `example.com` and `test.example.net` + would not match. \n Hostnames that are prefixed with a wildcard + label (`*.`) are interpreted as a suffix match. That means that + a match for `*.example.com` would match both `test.example.com`, + and `foo.test.example.com`, but not `example.com`. \n If both the + Listener and HTTPRoute have specified hostnames, any HTTPRoute hostnames + that do not match the Listener hostname MUST be ignored. For example, + if a Listener specified `*.example.com`, and the HTTPRoute specified + `test.example.com` and `test.example.net`, `test.example.net` must + not be considered for a match. \n If both the Listener and HTTPRoute + have specified hostnames, and none match with the criteria above, + then the HTTPRoute is not accepted. The implementation must raise + an 'Accepted' Condition with a status of `False` in the corresponding + RouteParentStatus. \n Support: Core" + items: + description: "Hostname is the fully qualified domain name of a network + host. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname + may be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n Hostname can be \"precise\" + which is a domain name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", which is a domain + name prefixed with a single wildcard label (e.g. `*.example.com`). + \n Note that as per RFC1035 and RFC1123, a *label* must consist + of lower case alphanumeric characters or '-', and must start and + end with an alphanumeric character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + parentRefs: + description: "ParentRefs references the resources (usually Gateways) + that a Route wants to be attached to. Note that the referenced parent + resource needs to allow this for the attachment to be complete. + For Gateways, that means the Gateway needs to allow attachment from + Routes of this kind and namespace. \n The only kind of parent resource + with \"Core\" support is Gateway. This API may be extended in the + future to support additional kinds of parent resources such as one + of the route kinds. \n It is invalid to reference an identical parent + more than once. It is valid to reference multiple distinct sections + within the same parent resource, such as 2 Listeners within a Gateway. + \n It is possible to separately reference multiple distinct objects + that may be collapsed by an implementation. For example, some implementations + may choose to merge compatible Gateway Listeners together. If that + is the case, the list of routes attached to those resources should + also be merged." + items: + description: "ParentReference identifies an API object (usually + a Gateway) that can be considered a parent of this resource (usually + a route). The only kind of parent resource with \"Core\" support + is Gateway. This API may be extended in the future to support + additional kinds of parent resources, such as HTTPRoute. \n The + API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid." + properties: + group: + default: policy.linkerd.io + description: "Group is the group of the referent. \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: Core + (Gateway) Support: Custom (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. When + unspecified (or empty string), this refers to the local namespace + of the Route. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: "port" + type: integer + sectionName: + description: "SectionName is the name of a section within the + target resource. In the following resources, SectionName is + interpreted as the following: \n * Gateway: Listener Name. + When both Port (experimental) and SectionName are specified, + the name and port of the selected listener must match both + specified values. \n Implementations MAY choose to support + attaching Routes to other resources. If that is the case, + they MUST clearly document how SectionName is interpreted. + \n When unspecified (empty string), this will reference the + entire resource. For the purpose of status, an attachment + is considered successful if at least one section in the parent + resource accepts it. For example, Gateway listeners can restrict + which Routes can attach to them by Route kind, namespace, + or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this + Route, the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + rules: + default: + - matches: + - path: + type: PathPrefix + value: / + description: Rules are a list of HTTP matchers, filters and actions. + items: + description: HTTPRouteRule defines semantics for matching an HTTP + request based on conditions (matches) and processing it (filters). + properties: + backendRefs: + type: array + items: + type: object + properties: + name: + type: string + port: + type: integer + namespace: + type: string + default: "default" + filters: + description: "Filters defined at this level should be + executed if and only if the request is being forwarded + to the backend defined here. \n Support: Implementation-specific + (For broader support of filters, use the Filters field + in HTTPRouteRule.)" + items: + description: HTTPRouteFilter defines processing steps + that must be completed during the request or response + lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway + implementations. Some examples include request or + response modification, implementing authentication + strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type + of the filter. + properties: + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema + for a filter that modifies request headers. \n + Support: Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestRedirect: + description: "RequestRedirect defines a schema for + a filter that responds to the request with an + HTTP redirection. \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be + used in the value of the `Location` header + in the response. When empty, the hostname + in the `Host` header of the request is used. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to + modify the path of the incoming request. The + modified path is then used to construct the + `Location` header. When empty, the request + path is used as-is. \n Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in + the value of the `Location` header in the + response. \n If no port is specified, the + redirect port MUST be derived using the following + rules: \n * If redirect scheme is not-empty, + the redirect port MUST be the well-known port + associated with the redirect scheme. Specifically + \"http\" to port 80 and \"https\" to port + 443. If the redirect scheme does not have + a well-known port, the listener port of the + Gateway SHOULD be used. * If redirect scheme + is empty, the redirect port MUST be the Gateway + Listener port. \n Implementations SHOULD NOT + add the port number in the 'Location' header + in the following cases: \n * A Location header + that will use HTTP (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 80. * A Location header that + will use HTTPS (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 443. \n Support: Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used + in the value of the `Location` header in the + response. When empty, the scheme of the request + is used. \n Scheme redirects can affect the + port of the redirect, for more information, + refer to the documentation for the port field + of this filter. \n Note that values may be + added to this enum, implementations must ensure + that unknown values will not cause a crash. + \n Unknown values here must result in the + implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`. \n Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status + code to be used in response. \n Note that + values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result + in the implementation setting the Accepted + Condition for the Route to `status: False`, + with a Reason of `UnsupportedValue`. \n Support: + Core" + enum: + - 301 + - 302 + type: integer + type: object + responseHeaderModifier: + description: "ResponseHeaderModifier defines a schema + for a filter that modifies response headers. \n + Support: Extended" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: "Type identifies the type of filter + to apply. As with other API fields, types are + classified into three conformance levels: \n - + Core: Filter types and their corresponding configuration + defined by \"Support: Core\" in this package, + e.g. \"RequestHeaderModifier\". All implementations + must support core filters. \n - Extended: Filter + types and their corresponding configuration defined + by \"Support: Extended\" in this package, e.g. + \"RequestMirror\". Implementers are encouraged + to support extended filters. \n - Implementation-specific: + Filters that are defined and supported by specific + vendors. In the future, filters showing convergence + in behavior across multiple implementations will + be considered for inclusion in extended or core + conformance levels. Filter-specific configuration + for such filters is specified using the ExtensionRef + field. `Type` should be set to \"ExtensionRef\" + for custom filters. \n Implementers are encouraged + to define custom implementation types to extend + the core API with implementation-specific behavior. + \n If a reference to a custom filter type cannot + be resolved, the filter MUST NOT be skipped. Instead, + requests that would have been processed by that + filter MUST receive a HTTP error response. \n + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result in + the implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestRedirect + type: string + required: + - type + type: object + maxItems: 16 + type: array + filters: + description: "Filters define the filters that are applied to + requests that match this rule. \n The effects of ordering + of multiple behaviors are currently unspecified. This can + change in the future based on feedback during the alpha stage. + \n Conformance-levels at this level are defined based on the + type of filter: \n - ALL core filters MUST be supported by + all implementations. - Implementers are encouraged to support + extended filters. - Implementation-specific custom filters + have no API guarantees across implementations. \n Specifying + a core filter multiple times has unspecified or custom conformance. + \n All filters are expected to be compatible with each other + except for the URLRewrite and RequestRedirect filters, which + may not be combined. If an implementation can not support + other combinations of filters, they must clearly document + that limitation. In all cases where incompatible or unsupported + filters are specified, implementations MUST add a warning + condition to status. \n Support: Core" + items: + description: HTTPRouteFilter defines processing steps that + must be completed during the request or response lifecycle. + HTTPRouteFilters are meant as an extension point to express + processing that may be done in Gateway implementations. + Some examples include request or response modification, + implementing authentication strategies, rate-limiting, and + traffic shaping. API guarantee/conformance is defined based + on the type of the filter. + properties: + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema for + a filter that modifies request headers. \n Support: + Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It appends + to any existing values associated with the header + name. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: add: - name: \"my-header\" value: + \"bar\" \n Output: GET /foo HTTP/1.1 my-header: + foo my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from the + HTTP request before the action. The value of Remove + is a list of HTTP header names. Note that the header + names are case-insensitive (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + \ my-header2: bar my-header3: baz \n Config: + \ remove: [\"my-header1\", \"my-header3\"] \n Output: + \ GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with the + given header (name, value) before the action. \n + Input: GET /foo HTTP/1.1 my-header: foo \n Config: + \ set: - name: \"my-header\" value: \"bar\" + \n Output: GET /foo HTTP/1.1 my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestRedirect: + description: "RequestRedirect defines a schema for a filter + that responds to the request with an HTTP redirection. + \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be used + in the value of the `Location` header in the response. + When empty, the hostname of the request is used. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to + modify the path of the incoming request. The + modified path is then used to construct the + `Location` header. When empty, the request + path is used as-is. \n Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in the value + of the `Location` header in the response. When empty, + port (if specified) of the request is used. \n Support: + Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used in the + value of the `Location` header in the response. + When empty, the scheme of the request is used. \n + Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status code to + be used in response. \n Support: Core" + enum: + - 301 + - 302 + type: integer + type: object + type: + description: "Type identifies the type of filter to apply. + As with other API fields, types are classified into + three conformance levels: \n - Core: Filter types and + their corresponding configuration defined by \"Support: + Core\" in this package, e.g. \"RequestHeaderModifier\"." + enum: + - RequestHeaderModifier + - RequestRedirect + type: string + required: + - type + type: object + maxItems: 16 + type: array + matches: + default: + - path: + type: PathPrefix + value: / + description: "Matches define conditions used for matching the + rule against incoming HTTP requests. Each match is independent, + i.e. this rule will be matched if **any** one of the matches + is satisfied. \n For example, take the following matches configuration: + \n ``` matches: - path: value: \"/foo\" headers: - + name: \"version\" value: \"v2\" - path: value: \"/v2/foo\" + ``` \n For a request to match against this rule, a request + must satisfy EITHER of the two conditions: \n - path prefixed + with `/foo` AND contains the header `version: v2` - path prefix + of `/v2/foo` \n See the documentation for HTTPRouteMatch on + how to specify multiple match conditions that should be ANDed + together. \n If no matches are specified, the default is a + prefix path match on \"/\", which has the effect of matching + every HTTP request. \n Proxy or Load Balancer routing configuration + generated from HTTPRoutes MUST prioritize rules based on the + following criteria, continuing on ties. Precedence must be + given to the the Rule with the largest number of: \n * Characters + in a matching non-wildcard hostname. * Characters in a matching + hostname. * Characters in a matching path. * Header matches. + * Query param matches. \n If ties still exist across multiple + Routes, matching precedence MUST be determined in order of + the following criteria, continuing on ties: \n * The oldest + Route based on creation timestamp. * The Route appearing first + in alphabetical order by \"{namespace}/{name}\". \n If ties + still exist within the Route that has been given precedence, + matching precedence MUST be granted to the first matching + rule meeting the above criteria. \n When no rules matching + a request have been successfully attached to the parent a + request is coming from, a HTTP 404 status code MUST be returned." + items: + description: "HTTPRouteMatch defines the predicate used to + match requests to a given action. Multiple match types are + ANDed together, i.e. the match will evaluate to true only + if all conditions are satisfied. \n For example, the match + below will match a HTTP request only if its path starts + with `/foo` AND it contains the `version: v1` header: \n + ``` match: path: value: \"/foo\" headers: - name: + \"version\" value \"v1\" ```" + properties: + headers: + description: Headers specifies HTTP request header matchers. + Multiple match values are ANDed together, meaning, a + request must match all the specified headers to select + the route. + items: + description: HTTPHeaderMatch describes how to select + a HTTP route by matching HTTP request headers. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case insensitive. + (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent header + names, only the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST be + ignored. Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered equivalent. + \n When a header is repeated in an HTTP request, + it is implementation-specific behavior as to how + this is represented. Generally, proxies should + follow the guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, with special + handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the header. \n Support: Core (Exact) + \n Support: Custom (RegularExpression) \n Since + RegularExpression HeaderMatchType has custom conformance, + implementations can support POSIX, PCRE or any + other dialects of regular expressions. Please + read the implementation's documentation to determine + the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP Header to + be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. When + specified, this route will be matched only if the request + has the specified method. \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path matcher. + If this field is not specified, a default prefix match + on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match against + the path Value. \n Support: Core (Exact, PathPrefix) + \n Support: Custom (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match against. + maxLength: 1024 + type: string + type: object + queryParams: + description: QueryParams specifies HTTP query parameter + matchers. Multiple match values are ANDed together, + meaning, a request must match all the specified query + parameters to select the route. + items: + description: HTTPQueryParamMatch describes how to select + a HTTP route by matching HTTP query parameters. + properties: + name: + description: Name is the name of the HTTP query + param to be matched. This must be an exact string + match. (See https://tools.ietf.org/html/rfc7230#section-2.7.3). + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the query parameter. \n Support: + Extended (Exact) \n Support: Custom (RegularExpression) + \n Since RegularExpression QueryParamMatchType + has custom conformance, implementations can support + POSIX, PCRE or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP query param + to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + maxItems: 8 + type: array + type: object + maxItems: 16 + type: array + type: object + status: + description: Status defines the current state of HTTPRoute. + properties: + parents: + description: "Parents is a list of parent resources (usually Gateways) + that are associated with the route, and the status of the route + with respect to each parent. When this route attaches to a parent, + the controller that manages the parent must add an entry to this + list when the controller first sees the route and should update + the entry as appropriate when the route or gateway is modified. + \n Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this + API can only populate Route status for the Gateways/parent resources + they are responsible for. \n A maximum of 32 Gateways will be represented + in this list. An empty list means the route has not been attached + to any Gateway." + items: + description: RouteParentStatus describes the status of a route with + respect to an associated Parent. + properties: + conditions: + description: "Conditions describes the status of the route with + respect to the Gateway. Note that the route's availability + is also subject to the Gateway's own status conditions and + listener status. \n If the Route's ParentRef specifies an + existing Gateway that supports Routes of this kind AND that + Gateway's controller has sufficient access, then that Gateway's + controller MUST set the \"Accepted\" condition on the Route, + to indicate whether the route has been accepted or rejected + by the Gateway, and why. \n A Route MUST be considered \"Accepted\" + if at least one of the Route's rules is implemented by the + Gateway. \n There are a number of cases where the \"Accepted\" + condition may not be set due to lack of controller visibility, + that includes when: \n * The Route refers to a non-existent + parent. * The Route is of a type that the controller does + not support. * The Route is in a namespace the the controller + does not have access to." + items: + description: "Condition contains details for one aspect of + the current state of this API Resource. --- This struct + is intended for direct use as an array at the field path + .status.conditions. For example, type FooStatus struct{ + \ // Represents the observations of a foo's current state. + \ // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type + \ // +patchStrategy=merge // +listType=map // + +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` + \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should + be when the underlying condition changed. If that is + not known, then using the time when the API field changed + is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, + if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the + current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier + indicating the reason for the condition's last transition. + Producers of specific condition types may define expected + values and meanings for this field, and whether the + values are considered a guaranteed API. The value should + be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across + resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability + to deconflict is important. The regex it matches is + (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: "ControllerName is a domain/path string that indicates + the name of the controller that wrote this status. This corresponds + with the controllerName field on GatewayClass. \n Example: + \"example.net/gateway-controller\". \n The format of this + field is DOMAIN \"/\" PATH, where DOMAIN and PATH are valid + Kubernetes names (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + \n Controllers MUST populate this field when writing status. + Controllers should ensure that entries to status populated + with their ControllerName are cleaned up when they are no + longer necessary." + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: ParentRef corresponds with a ParentRef in the spec + that this RouteParentStatus struct describes the status of. + properties: + group: + default: policy.linkerd.io + description: "Group is the group of the referent. \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: + Core (Gateway) Support: Custom (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. + When unspecified (or empty string), this refers to the + local namespace of the Route. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + sectionName: + description: "SectionName is the name of a section within + the target resource. In the following resources, SectionName + is interpreted as the following: \n * Gateway: Listener + Name. When both Port (experimental) and SectionName are + specified, the name and port of the selected listener + must match both specified values. \n Implementations MAY + choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName + is interpreted. \n When unspecified (empty string), this + will reference the entire resource. For the purpose of + status, an attachment is considered successful if at least + one section in the parent resource accepts it. For example, + Gateway listeners can restrict which Routes can attach + to them by Route kind, namespace, or hostname. If 1 of + 2 Gateway listeners accept attachment from the referencing + Route, the Route MUST be considered successfully attached. + If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.hostnames + name: Hostnames + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta2 + schema: + openAPIV3Schema: + description: HTTPRoute provides a way to route HTTP requests. This includes + the capability to match requests by hostname, path, header, or query param. + Filters can be used to specify additional processing steps. Backends specify + where matching requests should be routed. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of HTTPRoute. + properties: + hostnames: + description: "Hostnames defines a set of hostname that should match + against the HTTP Host header to select a HTTPRoute to process the + request. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname may + be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n If a hostname is specified + by both the Listener and HTTPRoute, there must be at least one intersecting + hostname for the HTTPRoute to be attached to the Listener. For example: + \n * A Listener with `test.example.com` as the hostname matches + HTTPRoutes that have either not specified any hostnames, or have + specified at least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches HTTPRoutes + \ that have either not specified any hostnames or have specified + at least one hostname that matches the Listener hostname. For + example, `*.example.com`, `test.example.com`, and `foo.test.example.com` + would all match. On the other hand, `example.com` and `test.example.net` + would not match. \n Hostnames that are prefixed with a wildcard + label (`*.`) are interpreted as a suffix match. That means that + a match for `*.example.com` would match both `test.example.com`, + and `foo.test.example.com`, but not `example.com`. \n If both the + Listener and HTTPRoute have specified hostnames, any HTTPRoute hostnames + that do not match the Listener hostname MUST be ignored. For example, + if a Listener specified `*.example.com`, and the HTTPRoute specified + `test.example.com` and `test.example.net`, `test.example.net` must + not be considered for a match. \n If both the Listener and HTTPRoute + have specified hostnames, and none match with the criteria above, + then the HTTPRoute is not accepted. The implementation must raise + an 'Accepted' Condition with a status of `False` in the corresponding + RouteParentStatus. \n Support: Core" + items: + description: "Hostname is the fully qualified domain name of a network + host. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname + may be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n Hostname can be \"precise\" + which is a domain name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", which is a domain + name prefixed with a single wildcard label (e.g. `*.example.com`). + \n Note that as per RFC1035 and RFC1123, a *label* must consist + of lower case alphanumeric characters or '-', and must start and + end with an alphanumeric character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + parentRefs: + description: "ParentRefs references the resources (usually Gateways) + that a Route wants to be attached to. Note that the referenced parent + resource needs to allow this for the attachment to be complete. + For Gateways, that means the Gateway needs to allow attachment from + Routes of this kind and namespace. \n The only kind of parent resource + with \"Core\" support is Gateway. This API may be extended in the + future to support additional kinds of parent resources such as one + of the route kinds. \n It is invalid to reference an identical parent + more than once. It is valid to reference multiple distinct sections + within the same parent resource, such as 2 Listeners within a Gateway. + \n It is possible to separately reference multiple distinct objects + that may be collapsed by an implementation. For example, some implementations + may choose to merge compatible Gateway Listeners together. If that + is the case, the list of routes attached to those resources should + also be merged." + items: + description: "ParentReference identifies an API object (usually + a Gateway) that can be considered a parent of this resource (usually + a route). The only kind of parent resource with \"Core\" support + is Gateway. This API may be extended in the future to support + additional kinds of parent resources, such as HTTPRoute. \n The + API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid." + properties: + group: + default: policy.linkerd.io + description: "Group is the group of the referent. \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: Core + (Gateway) Support: Custom (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. When + unspecified (or empty string), this refers to the local namespace + of the Route. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: "Port specifies the destination + port number to use for this resource. + Port is required when the referent is + a Kubernetes Service. In this case, the + port number is the service port number, + not the target port. For other resources, + destination port might be derived from + the referent resource or this field. \n Support: Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: "SectionName is the name of a section within the + target resource. In the following resources, SectionName is + interpreted as the following: \n * Gateway: Listener Name. + When both Port (experimental) and SectionName are specified, + the name and port of the selected listener must match both + specified values. \n Implementations MAY choose to support + attaching Routes to other resources. If that is the case, + they MUST clearly document how SectionName is interpreted. + \n When unspecified (empty string), this will reference the + entire resource. For the purpose of status, an attachment + is considered successful if at least one section in the parent + resource accepts it. For example, Gateway listeners can restrict + which Routes can attach to them by Route kind, namespace, + or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this + Route, the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + rules: + default: + - matches: + - path: + type: PathPrefix + value: / + description: Rules are a list of HTTP matchers, filters and actions. + items: + description: HTTPRouteRule defines semantics for matching an HTTP + request based on conditions (matches) and processing it (filters). + properties: + backendRefs: + description: "BackendRefs defines the backend(s) where matching + requests should be sent. \n Failure behavior here depends + on how many BackendRefs are specified and how many are invalid. + \n If *all* entries in BackendRefs are invalid, and there + are also no filters specified in this route rule, *all* traffic + which matches this rule MUST receive a 500 status code. \n + See the HTTPBackendRef definition for the rules about what + makes a single HTTPBackendRef invalid. \n When a HTTPBackendRef + is invalid, 500 status codes MUST be returned for requests + that would have otherwise been routed to an invalid backend. + If multiple backends are specified, and some are invalid, + the proportion of requests that would otherwise have been + routed to an invalid backend MUST receive a 500 status code. + \n For example, if two backends are specified with equal weights, + and one is invalid, 50 percent of traffic must receive a 500. + Implementations may choose how that 50 percent is determined. + \n Support: Core for Kubernetes Service \n Support: Implementation-specific + for any other resource \n Support for weight: Core" + items: + description: HTTPBackendRef defines how a HTTPRoute should + forward an HTTP request. + properties: + group: + default: "" + description: Group is the group of the referent. For example, + "gateway.networking.k8s.io". When unspecified or empty + string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: Kind is kind of the referent. For example + "HTTPRoute" or "Service". Defaults to "Service" when + not specified. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the backend. + When unspecified, the local namespace is inferred. \n + Note that when a namespace is specified, a ReferenceGrant + object is required in the referent namespace to allow + that namespace's owner to accept the reference. See + the ReferenceGrant documentation for details. \n Support: + Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: Port specifies the destination port number + to use for this resource. Port is required when the + referent is a Kubernetes Service. In this case, the + port number is the service port number, not the target + port. For other resources, destination port might be + derived from the referent resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: "Weight specifies the proportion of requests + forwarded to the referenced backend. This is computed + as weight/(sum of all weights in this BackendRefs list). + For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision + an implementation supports. Weight is not a percentage + and the sum of weights does not need to equal 100. \n + If only one backend is specified and it has a weight + greater than 0, 100% of the traffic is forwarded to + that backend. If weight is set to 0, no traffic should + be forwarded for this entry. If unspecified, weight + defaults to 1. \n Support for this field varies based + on the context where used." + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + filters: + description: "Filters defined at this level should be + executed if and only if the request is being forwarded + to the backend defined here. \n Support: Implementation-specific + (For broader support of filters, use the Filters field + in HTTPRouteRule.)" + items: + description: HTTPRouteFilter defines processing steps + that must be completed during the request or response + lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway + implementations. Some examples include request or + response modification, implementing authentication + strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type + of the filter. + properties: + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema + for a filter that modifies request headers. \n + Support: Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestRedirect: + description: "RequestRedirect defines a schema for + a filter that responds to the request with an + HTTP redirection. \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be + used in the value of the `Location` header + in the response. When empty, the hostname + in the `Host` header of the request is used. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to + modify the path of the incoming request. The + modified path is then used to construct the + `Location` header. When empty, the request + path is used as-is. \n Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in + the value of the `Location` header in the + response. \n If no port is specified, the + redirect port MUST be derived using the following + rules: \n * If redirect scheme is not-empty, + the redirect port MUST be the well-known port + associated with the redirect scheme. Specifically + \"http\" to port 80 and \"https\" to port + 443. If the redirect scheme does not have + a well-known port, the listener port of the + Gateway SHOULD be used. * If redirect scheme + is empty, the redirect port MUST be the Gateway + Listener port. \n Implementations SHOULD NOT + add the port number in the 'Location' header + in the following cases: \n * A Location header + that will use HTTP (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 80. * A Location header that + will use HTTPS (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 443. \n Support: Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used + in the value of the `Location` header in the + response. When empty, the scheme of the request + is used. \n Scheme redirects can affect the + port of the redirect, for more information, + refer to the documentation for the port field + of this filter. \n Note that values may be + added to this enum, implementations must ensure + that unknown values will not cause a crash. + \n Unknown values here must result in the + implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`. \n Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status + code to be used in response. \n Note that + values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result + in the implementation setting the Accepted + Condition for the Route to `status: False`, + with a Reason of `UnsupportedValue`. \n Support: + Core" + enum: + - 301 + - 302 + type: integer + type: object + responseHeaderModifier: + description: "ResponseHeaderModifier defines a schema + for a filter that modifies response headers. \n + Support: Extended" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: "Type identifies the type of filter + to apply. As with other API fields, types are + classified into three conformance levels: \n - + Core: Filter types and their corresponding configuration + defined by \"Support: Core\" in this package, + e.g. \"RequestHeaderModifier\". All implementations + must support core filters. \n - Extended: Filter + types and their corresponding configuration defined + by \"Support: Extended\" in this package, e.g. + \"RequestMirror\". Implementers are encouraged + to support extended filters. \n - Implementation-specific: + Filters that are defined and supported by specific + vendors. In the future, filters showing convergence + in behavior across multiple implementations will + be considered for inclusion in extended or core + conformance levels. Filter-specific configuration + for such filters is specified using the ExtensionRef + field. `Type` should be set to \"ExtensionRef\" + for custom filters. \n Implementers are encouraged + to define custom implementation types to extend + the core API with implementation-specific behavior. + \n If a reference to a custom filter type cannot + be resolved, the filter MUST NOT be skipped. Instead, + requests that would have been processed by that + filter MUST receive a HTTP error response. \n + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result in + the implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestRedirect + type: string + required: + - type + type: object + maxItems: 16 + type: array + required: + - name + type: object + maxItems: 16 + type: array + filters: + description: "Filters define the filters that are applied to + requests that match this rule. \n The effects of ordering + of multiple behaviors are currently unspecified. This can + change in the future based on feedback during the alpha stage. + \n Conformance-levels at this level are defined based on the + type of filter: \n - ALL core filters MUST be supported by + all implementations. - Implementers are encouraged to support + extended filters. - Implementation-specific custom filters + have no API guarantees across implementations. \n Specifying + a core filter multiple times has unspecified or custom conformance. + \n All filters are expected to be compatible with each other + except for the URLRewrite and RequestRedirect filters, which + may not be combined. If an implementation can not support + other combinations of filters, they must clearly document + that limitation. In all cases where incompatible or unsupported + filters are specified, implementations MUST add a warning + condition to status. \n Support: Core" + items: + description: HTTPRouteFilter defines processing steps that + must be completed during the request or response lifecycle. + HTTPRouteFilters are meant as an extension point to express + processing that may be done in Gateway implementations. + Some examples include request or response modification, + implementing authentication strategies, rate-limiting, and + traffic shaping. API guarantee/conformance is defined based + on the type of the filter. + properties: + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema for + a filter that modifies request headers. \n Support: + Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It appends + to any existing values associated with the header + name. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: add: - name: \"my-header\" value: + \"bar\" \n Output: GET /foo HTTP/1.1 my-header: + foo my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from the + HTTP request before the action. The value of Remove + is a list of HTTP header names. Note that the header + names are case-insensitive (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + \ my-header2: bar my-header3: baz \n Config: + \ remove: [\"my-header1\", \"my-header3\"] \n Output: + \ GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with the + given header (name, value) before the action. \n + Input: GET /foo HTTP/1.1 my-header: foo \n Config: + \ set: - name: \"my-header\" value: \"bar\" + \n Output: GET /foo HTTP/1.1 my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestRedirect: + description: "RequestRedirect defines a schema for a filter + that responds to the request with an HTTP redirection. + \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be used + in the value of the `Location` header in the response. + When empty, the hostname of the request is used. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to + modify the path of the incoming request. The + modified path is then used to construct the + `Location` header. When empty, the request + path is used as-is. \n Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in the value + of the `Location` header in the response. When empty, + port (if specified) of the request is used. \n Support: + Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used in the + value of the `Location` header in the response. + When empty, the scheme of the request is used. \n + Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status code to + be used in response. \n Support: Core" + enum: + - 301 + - 302 + type: integer + type: object + type: + description: "Type identifies the type of filter to apply. + As with other API fields, types are classified into + three conformance levels: \n - Core: Filter types and + their corresponding configuration defined by \"Support: + Core\" in this package, e.g. \"RequestHeaderModifier\"." + enum: + - RequestHeaderModifier + - RequestRedirect + type: string + required: + - type + type: object + maxItems: 16 + type: array + matches: + default: + - path: + type: PathPrefix + value: / + description: "Matches define conditions used for matching the + rule against incoming HTTP requests. Each match is independent, + i.e. this rule will be matched if **any** one of the matches + is satisfied. \n For example, take the following matches configuration: + \n ``` matches: - path: value: \"/foo\" headers: - + name: \"version\" value: \"v2\" - path: value: \"/v2/foo\" + ``` \n For a request to match against this rule, a request + must satisfy EITHER of the two conditions: \n - path prefixed + with `/foo` AND contains the header `version: v2` - path prefix + of `/v2/foo` \n See the documentation for HTTPRouteMatch on + how to specify multiple match conditions that should be ANDed + together. \n If no matches are specified, the default is a + prefix path match on \"/\", which has the effect of matching + every HTTP request. \n Proxy or Load Balancer routing configuration + generated from HTTPRoutes MUST prioritize rules based on the + following criteria, continuing on ties. Precedence must be + given to the the Rule with the largest number of: \n * Characters + in a matching non-wildcard hostname. * Characters in a matching + hostname. * Characters in a matching path. * Header matches. + * Query param matches. \n If ties still exist across multiple + Routes, matching precedence MUST be determined in order of + the following criteria, continuing on ties: \n * The oldest + Route based on creation timestamp. * The Route appearing first + in alphabetical order by \"{namespace}/{name}\". \n If ties + still exist within the Route that has been given precedence, + matching precedence MUST be granted to the first matching + rule meeting the above criteria. \n When no rules matching + a request have been successfully attached to the parent a + request is coming from, a HTTP 404 status code MUST be returned." + items: + description: "HTTPRouteMatch defines the predicate used to + match requests to a given action. Multiple match types are + ANDed together, i.e. the match will evaluate to true only + if all conditions are satisfied. \n For example, the match + below will match a HTTP request only if its path starts + with `/foo` AND it contains the `version: v1` header: \n + ``` match: path: value: \"/foo\" headers: - name: + \"version\" value \"v1\" ```" + properties: + headers: + description: Headers specifies HTTP request header matchers. + Multiple match values are ANDed together, meaning, a + request must match all the specified headers to select + the route. + items: + description: HTTPHeaderMatch describes how to select + a HTTP route by matching HTTP request headers. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case insensitive. + (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent header + names, only the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST be + ignored. Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered equivalent. + \n When a header is repeated in an HTTP request, + it is implementation-specific behavior as to how + this is represented. Generally, proxies should + follow the guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, with special + handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the header. \n Support: Core (Exact) + \n Support: Custom (RegularExpression) \n Since + RegularExpression HeaderMatchType has custom conformance, + implementations can support POSIX, PCRE or any + other dialects of regular expressions. Please + read the implementation's documentation to determine + the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP Header to + be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. When + specified, this route will be matched only if the request + has the specified method. \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path matcher. + If this field is not specified, a default prefix match + on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match against + the path Value. \n Support: Core (Exact, PathPrefix) + \n Support: Custom (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match against. + maxLength: 1024 + type: string + type: object + queryParams: + description: QueryParams specifies HTTP query parameter + matchers. Multiple match values are ANDed together, + meaning, a request must match all the specified query + parameters to select the route. + items: + description: HTTPQueryParamMatch describes how to select + a HTTP route by matching HTTP query parameters. + properties: + name: + description: Name is the name of the HTTP query + param to be matched. This must be an exact string + match. (See https://tools.ietf.org/html/rfc7230#section-2.7.3). + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the query parameter. \n Support: + Extended (Exact) \n Support: Custom (RegularExpression) + \n Since RegularExpression QueryParamMatchType + has custom conformance, implementations can support + POSIX, PCRE or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP query param + to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + maxItems: 8 + type: array + type: object + maxItems: 16 + type: array + type: object + status: + description: Status defines the current state of HTTPRoute. + properties: + parents: + description: "Parents is a list of parent resources (usually Gateways) + that are associated with the route, and the status of the route + with respect to each parent. When this route attaches to a parent, + the controller that manages the parent must add an entry to this + list when the controller first sees the route and should update + the entry as appropriate when the route or gateway is modified. + \n Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this + API can only populate Route status for the Gateways/parent resources + they are responsible for. \n A maximum of 32 Gateways will be represented + in this list. An empty list means the route has not been attached + to any Gateway." + items: + description: RouteParentStatus describes the status of a route with + respect to an associated Parent. + properties: + conditions: + description: "Conditions describes the status of the route with + respect to the Gateway. Note that the route's availability + is also subject to the Gateway's own status conditions and + listener status. \n If the Route's ParentRef specifies an + existing Gateway that supports Routes of this kind AND that + Gateway's controller has sufficient access, then that Gateway's + controller MUST set the \"Accepted\" condition on the Route, + to indicate whether the route has been accepted or rejected + by the Gateway, and why. \n A Route MUST be considered \"Accepted\" + if at least one of the Route's rules is implemented by the + Gateway. \n There are a number of cases where the \"Accepted\" + condition may not be set due to lack of controller visibility, + that includes when: \n * The Route refers to a non-existent + parent. * The Route is of a type that the controller does + not support. * The Route is in a namespace the the controller + does not have access to." + items: + description: "Condition contains details for one aspect of + the current state of this API Resource. --- This struct + is intended for direct use as an array at the field path + .status.conditions. For example, type FooStatus struct{ + \ // Represents the observations of a foo's current state. + \ // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type + \ // +patchStrategy=merge // +listType=map // + +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` + \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should + be when the underlying condition changed. If that is + not known, then using the time when the API field changed + is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, + if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the + current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier + indicating the reason for the condition's last transition. + Producers of specific condition types may define expected + values and meanings for this field, and whether the + values are considered a guaranteed API. The value should + be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across + resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability + to deconflict is important. The regex it matches is + (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: "ControllerName is a domain/path string that indicates + the name of the controller that wrote this status. This corresponds + with the controllerName field on GatewayClass. \n Example: + \"example.net/gateway-controller\". \n The format of this + field is DOMAIN \"/\" PATH, where DOMAIN and PATH are valid + Kubernetes names (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + \n Controllers MUST populate this field when writing status. + Controllers should ensure that entries to status populated + with their ControllerName are cleaned up when they are no + longer necessary." + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: ParentRef corresponds with a ParentRef in the spec + that this RouteParentStatus struct describes the status of. + properties: + group: + default: policy.linkerd.io + description: "Group is the group of the referent. \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: + Core (Gateway) Support: Custom (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. + When unspecified (or empty string), this refers to the + local namespace of the Route. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + sectionName: + description: "SectionName is the name of a section within + the target resource. In the following resources, SectionName + is interpreted as the following: \n * Gateway: Listener + Name. When both Port (experimental) and SectionName are + specified, the name and port of the selected listener + must match both specified values. \n Implementations MAY + choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName + is interpreted. \n When unspecified (empty string), this + will reference the entire resource. For the purpose of + status, an attachment is considered successful if at least + one section in the parent resource accepts it. For example, + Gateway listeners can restrict which Routes can attach + to them by Route kind, namespace, or hostname. If 1 of + 2 Gateway listeners accept attachment from the referencing + Route, the Route MUST be considered successfully attached. + If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.hostnames + name: Hostnames + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta3 + schema: + openAPIV3Schema: + description: HTTPRoute provides a way to route HTTP requests. This includes + the capability to match requests by hostname, path, header, or query param. + Filters can be used to specify additional processing steps. Backends specify + where matching requests should be routed. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of HTTPRoute. + properties: + hostnames: + description: "Hostnames defines a set of hostname that should match + against the HTTP Host header to select a HTTPRoute to process the + request. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname may + be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n If a hostname is specified + by both the Listener and HTTPRoute, there must be at least one intersecting + hostname for the HTTPRoute to be attached to the Listener. For example: + \n * A Listener with `test.example.com` as the hostname matches + HTTPRoutes that have either not specified any hostnames, or have + specified at least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches HTTPRoutes + \ that have either not specified any hostnames or have specified + at least one hostname that matches the Listener hostname. For + example, `*.example.com`, `test.example.com`, and `foo.test.example.com` + would all match. On the other hand, `example.com` and `test.example.net` + would not match. \n Hostnames that are prefixed with a wildcard + label (`*.`) are interpreted as a suffix match. That means that + a match for `*.example.com` would match both `test.example.com`, + and `foo.test.example.com`, but not `example.com`. \n If both the + Listener and HTTPRoute have specified hostnames, any HTTPRoute hostnames + that do not match the Listener hostname MUST be ignored. For example, + if a Listener specified `*.example.com`, and the HTTPRoute specified + `test.example.com` and `test.example.net`, `test.example.net` must + not be considered for a match. \n If both the Listener and HTTPRoute + have specified hostnames, and none match with the criteria above, + then the HTTPRoute is not accepted. The implementation must raise + an 'Accepted' Condition with a status of `False` in the corresponding + RouteParentStatus. \n Support: Core" + items: + description: "Hostname is the fully qualified domain name of a network + host. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname + may be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n Hostname can be \"precise\" + which is a domain name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", which is a domain + name prefixed with a single wildcard label (e.g. `*.example.com`). + \n Note that as per RFC1035 and RFC1123, a *label* must consist + of lower case alphanumeric characters or '-', and must start and + end with an alphanumeric character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + parentRefs: + description: "ParentRefs references the resources (usually Gateways) + that a Route wants to be attached to. Note that the referenced parent + resource needs to allow this for the attachment to be complete. + For Gateways, that means the Gateway needs to allow attachment from + Routes of this kind and namespace. \n The only kind of parent resource + with \"Core\" support is Gateway. This API may be extended in the + future to support additional kinds of parent resources such as one + of the route kinds. \n It is invalid to reference an identical parent + more than once. It is valid to reference multiple distinct sections + within the same parent resource, such as 2 Listeners within a Gateway. + \n It is possible to separately reference multiple distinct objects + that may be collapsed by an implementation. For example, some implementations + may choose to merge compatible Gateway Listeners together. If that + is the case, the list of routes attached to those resources should + also be merged." + items: + description: "ParentReference identifies an API object (usually + a Gateway) that can be considered a parent of this resource (usually + a route). The only kind of parent resource with \"Core\" support + is Gateway. This API may be extended in the future to support + additional kinds of parent resources, such as HTTPRoute. \n The + API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid." + properties: + group: + default: policy.linkerd.io + description: "Group is the group of the referent. \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: Core + (Gateway) Support: Custom (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. When + unspecified (or empty string), this refers to the local namespace + of the Route. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: "Port specifies the destination + port number to use for this resource. + Port is required when the referent is + a Kubernetes Service. In this case, the + port number is the service port number, + not the target port. For other resources, + destination port might be derived from + the referent resource or this field. \n Support: Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: "SectionName is the name of a section within the + target resource. In the following resources, SectionName is + interpreted as the following: \n * Gateway: Listener Name. + When both Port (experimental) and SectionName are specified, + the name and port of the selected listener must match both + specified values. \n Implementations MAY choose to support + attaching Routes to other resources. If that is the case, + they MUST clearly document how SectionName is interpreted. + \n When unspecified (empty string), this will reference the + entire resource. For the purpose of status, an attachment + is considered successful if at least one section in the parent + resource accepts it. For example, Gateway listeners can restrict + which Routes can attach to them by Route kind, namespace, + or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this + Route, the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + rules: + default: + - matches: + - path: + type: PathPrefix + value: / + description: Rules are a list of HTTP matchers, filters and actions. + items: + description: HTTPRouteRule defines semantics for matching an HTTP + request based on conditions (matches) and processing it (filters). + properties: + backendRefs: + description: "BackendRefs defines the backend(s) where matching + requests should be sent. \n Failure behavior here depends + on how many BackendRefs are specified and how many are invalid. + \n If *all* entries in BackendRefs are invalid, and there + are also no filters specified in this route rule, *all* traffic + which matches this rule MUST receive a 500 status code. \n + See the HTTPBackendRef definition for the rules about what + makes a single HTTPBackendRef invalid. \n When a HTTPBackendRef + is invalid, 500 status codes MUST be returned for requests + that would have otherwise been routed to an invalid backend. + If multiple backends are specified, and some are invalid, + the proportion of requests that would otherwise have been + routed to an invalid backend MUST receive a 500 status code. + \n For example, if two backends are specified with equal weights, + and one is invalid, 50 percent of traffic must receive a 500. + Implementations may choose how that 50 percent is determined. + \n Support: Core for Kubernetes Service \n Support: Implementation-specific + for any other resource \n Support for weight: Core" + items: + description: HTTPBackendRef defines how a HTTPRoute should + forward an HTTP request. + properties: + group: + default: "" + description: Group is the group of the referent. For example, + "gateway.networking.k8s.io". When unspecified or empty + string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: Kind is kind of the referent. For example + "HTTPRoute" or "Service". Defaults to "Service" when + not specified. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the backend. + When unspecified, the local namespace is inferred. \n + Note that when a namespace is specified, a ReferenceGrant + object is required in the referent namespace to allow + that namespace's owner to accept the reference. See + the ReferenceGrant documentation for details. \n Support: + Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: Port specifies the destination port number + to use for this resource. Port is required when the + referent is a Kubernetes Service. In this case, the + port number is the service port number, not the target + port. For other resources, destination port might be + derived from the referent resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: "Weight specifies the proportion of requests + forwarded to the referenced backend. This is computed + as weight/(sum of all weights in this BackendRefs list). + For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision + an implementation supports. Weight is not a percentage + and the sum of weights does not need to equal 100. \n + If only one backend is specified and it has a weight + greater than 0, 100% of the traffic is forwarded to + that backend. If weight is set to 0, no traffic should + be forwarded for this entry. If unspecified, weight + defaults to 1. \n Support for this field varies based + on the context where used." + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + filters: + description: "Filters defined at this level should be + executed if and only if the request is being forwarded + to the backend defined here. \n Support: Implementation-specific + (For broader support of filters, use the Filters field + in HTTPRouteRule.)" + items: + description: HTTPRouteFilter defines processing steps + that must be completed during the request or response + lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway + implementations. Some examples include request or + response modification, implementing authentication + strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type + of the filter. + properties: + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema + for a filter that modifies request headers. \n + Support: Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestRedirect: + description: "RequestRedirect defines a schema for + a filter that responds to the request with an + HTTP redirection. \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be + used in the value of the `Location` header + in the response. When empty, the hostname + in the `Host` header of the request is used. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to + modify the path of the incoming request. The + modified path is then used to construct the + `Location` header. When empty, the request + path is used as-is. \n Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in + the value of the `Location` header in the + response. \n If no port is specified, the + redirect port MUST be derived using the following + rules: \n * If redirect scheme is not-empty, + the redirect port MUST be the well-known port + associated with the redirect scheme. Specifically + \"http\" to port 80 and \"https\" to port + 443. If the redirect scheme does not have + a well-known port, the listener port of the + Gateway SHOULD be used. * If redirect scheme + is empty, the redirect port MUST be the Gateway + Listener port. \n Implementations SHOULD NOT + add the port number in the 'Location' header + in the following cases: \n * A Location header + that will use HTTP (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 80. * A Location header that + will use HTTPS (whether that is determined + via the Listener protocol or the Scheme field) + _and_ use port 443. \n Support: Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used + in the value of the `Location` header in the + response. When empty, the scheme of the request + is used. \n Scheme redirects can affect the + port of the redirect, for more information, + refer to the documentation for the port field + of this filter. \n Note that values may be + added to this enum, implementations must ensure + that unknown values will not cause a crash. + \n Unknown values here must result in the + implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`. \n Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status + code to be used in response. \n Note that + values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result + in the implementation setting the Accepted + Condition for the Route to `status: False`, + with a Reason of `UnsupportedValue`. \n Support: + Core" + enum: + - 301 + - 302 + type: integer + type: object + responseHeaderModifier: + description: "ResponseHeaderModifier defines a schema + for a filter that modifies response headers. \n + Support: Extended" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It + appends to any existing values associated + with the header name. \n Input: GET /foo HTTP/1.1 + my-header: foo \n Config: add: - name: \"my-header\" + value: \"bar,baz\" \n Output: GET /foo HTTP/1.1 + my-header: foo,bar,baz" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from + the HTTP request before the action. The value + of Remove is a list of HTTP header names. + Note that the header names are case-insensitive + (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + my-header2: bar my-header3: baz \n Config: + remove: [\"my-header1\", \"my-header3\"] \n + Output: GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with + the given header (name, value) before the + action. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: set: - name: \"my-header\" + value: \"bar\" \n Output: GET /foo HTTP/1.1 + my-header: bar" + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an + equivalent name MUST be considered for + a match. Subsequent entries with an + equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: "Type identifies the type of filter + to apply. As with other API fields, types are + classified into three conformance levels: \n - + Core: Filter types and their corresponding configuration + defined by \"Support: Core\" in this package, + e.g. \"RequestHeaderModifier\". All implementations + must support core filters. \n - Extended: Filter + types and their corresponding configuration defined + by \"Support: Extended\" in this package, e.g. + \"RequestMirror\". Implementers are encouraged + to support extended filters. \n - Implementation-specific: + Filters that are defined and supported by specific + vendors. In the future, filters showing convergence + in behavior across multiple implementations will + be considered for inclusion in extended or core + conformance levels. Filter-specific configuration + for such filters is specified using the ExtensionRef + field. `Type` should be set to \"ExtensionRef\" + for custom filters. \n Implementers are encouraged + to define custom implementation types to extend + the core API with implementation-specific behavior. + \n If a reference to a custom filter type cannot + be resolved, the filter MUST NOT be skipped. Instead, + requests that would have been processed by that + filter MUST receive a HTTP error response. \n + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause + a crash. \n Unknown values here must result in + the implementation setting the Accepted Condition + for the Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestRedirect + type: string + required: + - type + type: object + maxItems: 16 + type: array + required: + - name + type: object + maxItems: 16 + type: array + filters: + description: "Filters define the filters that are applied to + requests that match this rule. \n The effects of ordering + of multiple behaviors are currently unspecified. This can + change in the future based on feedback during the alpha stage. + \n Conformance-levels at this level are defined based on the + type of filter: \n - ALL core filters MUST be supported by + all implementations. - Implementers are encouraged to support + extended filters. - Implementation-specific custom filters + have no API guarantees across implementations. \n Specifying + a core filter multiple times has unspecified or custom conformance. + \n All filters are expected to be compatible with each other + except for the URLRewrite and RequestRedirect filters, which + may not be combined. If an implementation can not support + other combinations of filters, they must clearly document + that limitation. In all cases where incompatible or unsupported + filters are specified, implementations MUST add a warning + condition to status. \n Support: Core" + items: + description: HTTPRouteFilter defines processing steps that + must be completed during the request or response lifecycle. + HTTPRouteFilters are meant as an extension point to express + processing that may be done in Gateway implementations. + Some examples include request or response modification, + implementing authentication strategies, rate-limiting, and + traffic shaping. API guarantee/conformance is defined based + on the type of the filter. + properties: + requestHeaderModifier: + description: "RequestHeaderModifier defines a schema for + a filter that modifies request headers. \n Support: + Core" + properties: + add: + description: "Add adds the given header(s) (name, + value) to the request before the action. It appends + to any existing values associated with the header + name. \n Input: GET /foo HTTP/1.1 my-header: + foo \n Config: add: - name: \"my-header\" value: + \"bar\" \n Output: GET /foo HTTP/1.1 my-header: + foo my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: "Remove the given header(s) from the + HTTP request before the action. The value of Remove + is a list of HTTP header names. Note that the header + names are case-insensitive (see https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + \n Input: GET /foo HTTP/1.1 my-header1: foo + \ my-header2: bar my-header3: baz \n Config: + \ remove: [\"my-header1\", \"my-header3\"] \n Output: + \ GET /foo HTTP/1.1 my-header2: bar" + items: + type: string + maxItems: 16 + type: array + set: + description: "Set overwrites the request with the + given header (name, value) before the action. \n + Input: GET /foo HTTP/1.1 my-header: foo \n Config: + \ set: - name: \"my-header\" value: \"bar\" + \n Output: GET /foo HTTP/1.1 my-header: bar" + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case + insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST + be ignored. Due to the case-insensitivity + of header names, \"foo\" and \"Foo\" are considered + equivalent." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestRedirect: + description: "RequestRedirect defines a schema for a filter + that responds to the request with an HTTP redirection. + \n Support: Core" + properties: + hostname: + description: "Hostname is the hostname to be used + in the value of the `Location` header in the response. + When empty, the hostname of the request is used. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: "Path defines parameters used to + modify the path of the incoming request. The + modified path is then used to construct the + `Location` header. When empty, the request + path is used as-is. \n Support: Extended" + properties: + replaceFullPath: + description: ReplaceFullPath specifies the + value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: "ReplacePrefixMatch specifies + the value with which to replace the prefix + match of a request during a rewrite or + redirect. For example, a request to \"/foo/bar\" + with a prefix match of \"/foo\" and a + ReplacePrefixMatch of \"/xyz\" would be + modified to \"/xyz/bar\". \n Note that + this matches the behavior of the PathPrefix + match type. This matches full path elements. + A path element refers to the list of labels + in the path split by the `/` separator. + When specified, a trailing `/` is ignored. + For example, the paths `/abc`, `/abc/`, + and `/abc/def` would all match the prefix + `/abc`, but the path `/abcd` would not. + \n Request Path | Prefix Match | Replace + Prefix | Modified Path -------------|--------------|----------------|---------- + /foo/bar | /foo | /xyz | + /xyz/bar /foo/bar | /foo | + /xyz/ | /xyz/bar /foo/bar | + /foo/ | /xyz | /xyz/bar + /foo/bar | /foo/ | /xyz/ | + /xyz/bar /foo | /foo | + /xyz | /xyz /foo/ | /foo + \ | /xyz | /xyz/ /foo/bar + \ | /foo | | + /bar /foo/ | /foo | | / /foo | /foo | + | / /foo/ | /foo + \ | / | / /foo | + /foo | / | /" + maxLength: 1024 + type: string + type: + description: "Type defines the type of path + modifier. Additional types may be added + in a future release of the API. \n Note + that values may be added to this enum, + implementations must ensure that unknown + values will not cause a crash. \n Unknown + values here must result in the implementation + setting the Accepted Condition for the + Route to `status: False`, with a Reason + of `UnsupportedValue`." + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + port: + description: "Port is the port to be used in the value + of the `Location` header in the response. When empty, + port (if specified) of the request is used. \n Support: + Extended" + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: "Scheme is the scheme to be used in the + value of the `Location` header in the response. + When empty, the scheme of the request is used. \n + Support: Extended" + enum: + - http + - https + type: string + statusCode: + default: 302 + description: "StatusCode is the HTTP status code to + be used in response. \n Support: Core" + enum: + - 301 + - 302 + type: integer + type: object + type: + description: "Type identifies the type of filter to apply. + As with other API fields, types are classified into + three conformance levels: \n - Core: Filter types and + their corresponding configuration defined by \"Support: + Core\" in this package, e.g. \"RequestHeaderModifier\"." + enum: + - RequestHeaderModifier + - RequestRedirect + type: string + required: + - type + type: object + maxItems: 16 + type: array + matches: + default: + - path: + type: PathPrefix + value: / + description: "Matches define conditions used for matching the + rule against incoming HTTP requests. Each match is independent, + i.e. this rule will be matched if **any** one of the matches + is satisfied. \n For example, take the following matches configuration: + \n ``` matches: - path: value: \"/foo\" headers: - + name: \"version\" value: \"v2\" - path: value: \"/v2/foo\" + ``` \n For a request to match against this rule, a request + must satisfy EITHER of the two conditions: \n - path prefixed + with `/foo` AND contains the header `version: v2` - path prefix + of `/v2/foo` \n See the documentation for HTTPRouteMatch on + how to specify multiple match conditions that should be ANDed + together. \n If no matches are specified, the default is a + prefix path match on \"/\", which has the effect of matching + every HTTP request. \n Proxy or Load Balancer routing configuration + generated from HTTPRoutes MUST prioritize rules based on the + following criteria, continuing on ties. Precedence must be + given to the the Rule with the largest number of: \n * Characters + in a matching non-wildcard hostname. * Characters in a matching + hostname. * Characters in a matching path. * Header matches. + * Query param matches. \n If ties still exist across multiple + Routes, matching precedence MUST be determined in order of + the following criteria, continuing on ties: \n * The oldest + Route based on creation timestamp. * The Route appearing first + in alphabetical order by \"{namespace}/{name}\". \n If ties + still exist within the Route that has been given precedence, + matching precedence MUST be granted to the first matching + rule meeting the above criteria. \n When no rules matching + a request have been successfully attached to the parent a + request is coming from, a HTTP 404 status code MUST be returned." + items: + description: "HTTPRouteMatch defines the predicate used to + match requests to a given action. Multiple match types are + ANDed together, i.e. the match will evaluate to true only + if all conditions are satisfied. \n For example, the match + below will match a HTTP request only if its path starts + with `/foo` AND it contains the `version: v1` header: \n + ``` match: path: value: \"/foo\" headers: - name: + \"version\" value \"v1\" ```" + properties: + headers: + description: Headers specifies HTTP request header matchers. + Multiple match values are ANDed together, meaning, a + request must match all the specified headers to select + the route. + items: + description: HTTPHeaderMatch describes how to select + a HTTP route by matching HTTP request headers. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case insensitive. + (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent header + names, only the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST be + ignored. Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered equivalent. + \n When a header is repeated in an HTTP request, + it is implementation-specific behavior as to how + this is represented. Generally, proxies should + follow the guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, with special + handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the header. \n Support: Core (Exact) + \n Support: Custom (RegularExpression) \n Since + RegularExpression HeaderMatchType has custom conformance, + implementations can support POSIX, PCRE or any + other dialects of regular expressions. Please + read the implementation's documentation to determine + the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP Header to + be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. When + specified, this route will be matched only if the request + has the specified method. \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path matcher. + If this field is not specified, a default prefix match + on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match against + the path Value. \n Support: Core (Exact, PathPrefix) + \n Support: Custom (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match against. + maxLength: 1024 + type: string + type: object + queryParams: + description: QueryParams specifies HTTP query parameter + matchers. Multiple match values are ANDed together, + meaning, a request must match all the specified query + parameters to select the route. + items: + description: HTTPQueryParamMatch describes how to select + a HTTP route by matching HTTP query parameters. + properties: + name: + description: Name is the name of the HTTP query + param to be matched. This must be an exact string + match. (See https://tools.ietf.org/html/rfc7230#section-2.7.3). + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the query parameter. \n Support: + Extended (Exact) \n Support: Custom (RegularExpression) + \n Since RegularExpression QueryParamMatchType + has custom conformance, implementations can support + POSIX, PCRE or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP query param + to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + maxItems: 8 + type: array + timeouts: + description: "Timeouts defines the timeouts that can be configured + for an HTTP request. \n Support: Core \n " + properties: + backendRequest: + description: "BackendRequest specifies a timeout for an + individual request from the gateway to a backend service. + Typically used in conjunction with automatic retries, + if supported by an implementation. Default is the value + of Request timeout. \n Support: Extended" + format: duration + type: string + request: + description: "Request specifies a timeout for responding + to client HTTP requests, disabled by default. \n For example, + the following rule will timeout if a client request is + taking longer than 10 seconds to complete: \n ``` rules: + - timeouts: request: 10s backendRefs: ... ``` \n Support: + Core" + format: duration + type: string + type: object + type: object + maxItems: 16 + type: array + type: object + status: + description: Status defines the current state of HTTPRoute. + properties: + parents: + description: "Parents is a list of parent resources (usually Gateways) + that are associated with the route, and the status of the route + with respect to each parent. When this route attaches to a parent, + the controller that manages the parent must add an entry to this + list when the controller first sees the route and should update + the entry as appropriate when the route or gateway is modified. + \n Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this + API can only populate Route status for the Gateways/parent resources + they are responsible for. \n A maximum of 32 Gateways will be represented + in this list. An empty list means the route has not been attached + to any Gateway." + items: + description: RouteParentStatus describes the status of a route with + respect to an associated Parent. + properties: + conditions: + description: "Conditions describes the status of the route with + respect to the Gateway. Note that the route's availability + is also subject to the Gateway's own status conditions and + listener status. \n If the Route's ParentRef specifies an + existing Gateway that supports Routes of this kind AND that + Gateway's controller has sufficient access, then that Gateway's + controller MUST set the \"Accepted\" condition on the Route, + to indicate whether the route has been accepted or rejected + by the Gateway, and why. \n A Route MUST be considered \"Accepted\" + if at least one of the Route's rules is implemented by the + Gateway. \n There are a number of cases where the \"Accepted\" + condition may not be set due to lack of controller visibility, + that includes when: \n * The Route refers to a non-existent + parent. * The Route is of a type that the controller does + not support. * The Route is in a namespace the the controller + does not have access to." + items: + description: "Condition contains details for one aspect of + the current state of this API Resource. --- This struct + is intended for direct use as an array at the field path + .status.conditions. For example, type FooStatus struct{ + \ // Represents the observations of a foo's current state. + \ // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type + \ // +patchStrategy=merge // +listType=map // + +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` + \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should + be when the underlying condition changed. If that is + not known, then using the time when the API field changed + is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, + if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the + current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier + indicating the reason for the condition's last transition. + Producers of specific condition types may define expected + values and meanings for this field, and whether the + values are considered a guaranteed API. The value should + be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across + resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability + to deconflict is important. The regex it matches is + (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: "ControllerName is a domain/path string that indicates + the name of the controller that wrote this status. This corresponds + with the controllerName field on GatewayClass. \n Example: + \"example.net/gateway-controller\". \n The format of this + field is DOMAIN \"/\" PATH, where DOMAIN and PATH are valid + Kubernetes names (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + \n Controllers MUST populate this field when writing status. + Controllers should ensure that entries to status populated + with their ControllerName are cleaned up when they are no + longer necessary." + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: ParentRef corresponds with a ParentRef in the spec + that this RouteParentStatus struct describes the status of. + properties: + group: + default: policy.linkerd.io + description: "Group is the group of the referent. \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: + Core (Gateway) Support: Custom (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. + When unspecified (or empty string), this refers to the + local namespace of the Route. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + sectionName: + description: "SectionName is the name of a section within + the target resource. In the following resources, SectionName + is interpreted as the following: \n * Gateway: Listener + Name. When both Port (experimental) and SectionName are + specified, the name and port of the selected listener + must match both specified values. \n Implementations MAY + choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName + is interpreted. \n When unspecified (empty string), this + will reference the entire resource. For the purpose of + status, an attachment is considered successful if at least + one section in the parent resource accepts it. For example, + Gateway listeners can restrict which Routes can attach + to them by Route kind, namespace, or hostname. If 1 of + 2 Gateway listeners accept attachment from the referencing + Route, the Route MUST be considered successfully attached. + If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/linkerd/charts/linkerd-crds/templates/policy/meshtls-authentication.yaml b/linkerd/charts/linkerd-crds/templates/policy/meshtls-authentication.yaml new file mode 100644 index 0000000..6840d5e --- /dev/null +++ b/linkerd/charts/linkerd-crds/templates/policy/meshtls-authentication.yaml @@ -0,0 +1,88 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: meshtlsauthentications.policy.linkerd.io + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + linkerd.io/control-plane-ns: {{.Release.Namespace}} +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: MeshTLSAuthentication + plural: meshtlsauthentications + singular: meshtlsauthentication + shortNames: [meshtlsauthn] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + MeshTLSAuthentication defines a list of authenticated client IDs + to be referenced by an `AuthorizationPolicy`. If a client + connection has the mutually-authenticated identity that matches + ANY of the of the provided identities, the connection is + considered authenticated. + type: object + oneOf: + - required: [identities] + - required: [identityRefs] + properties: + identities: + description: >- + Authorizes clients with the provided proxy identity strings + (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + minItems: 1 + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + identityRefs: + type: array + minItems: 1 + items: + type: object + required: + - kind + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. When unspecified, + this refers to all resources of the specified Group + and Kind in the specified namespace. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string diff --git a/linkerd/charts/linkerd-crds/templates/policy/network-authentication.yaml b/linkerd/charts/linkerd-crds/templates/policy/network-authentication.yaml new file mode 100644 index 0000000..cef15d3 --- /dev/null +++ b/linkerd/charts/linkerd-crds/templates/policy/network-authentication.yaml @@ -0,0 +1,53 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: networkauthentications.policy.linkerd.io + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + linkerd.io/control-plane-ns: {{.Release.Namespace}} +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: NetworkAuthentication + plural: networkauthentications + singular: networkauthentication + shortNames: [netauthn, networkauthn] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + NetworkAuthentication defines a list of authenticated client + networks to be referenced by an `AuthorizationPolicy`. If a + client connection originates from ANY of the of the provided + networks, the connection is considered authenticated. + type: object + required: [networks] + properties: + networks: + type: array + items: + type: object + required: [cidr] + properties: + cidr: + description: >- + The CIDR of the network to be authorized. + type: string + except: + description: >- + A list of IP networks/addresses not to be included in + the above `cidr`. + type: array + items: + type: string diff --git a/linkerd/charts/linkerd-crds/templates/policy/server-authorization.yaml b/linkerd/charts/linkerd-crds/templates/policy/server-authorization.yaml new file mode 100644 index 0000000..33fb659 --- /dev/null +++ b/linkerd/charts/linkerd-crds/templates/policy/server-authorization.yaml @@ -0,0 +1,266 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: serverauthorizations.policy.linkerd.io + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + linkerd.io/control-plane-ns: {{.Release.Namespace}} +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: ServerAuthorization + plural: serverauthorizations + singular: serverauthorization + shortNames: [saz, serverauthz, srvauthz] + versions: + - name: v1alpha1 + served: true + storage: false + deprecated: true + deprecationWarning: "policy.linkerd.io/v1alpha1 ServerAuthorization is deprecated; use policy.linkerd.io/v1beta1 ServerAuthorization" + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied servers. + type: object + required: [server, client] + properties: + server: + description: >- + Identifies servers in the same namespace for which this + authorization applies. + + Only one of `name` or `selector` may be specified. + type: object + oneOf: + - required: [name] + - required: [selector] + properties: + name: + description: References a `Server` instance by name + type: string + pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' + selector: + description: >- + A label query over servers on which this authorization applies. + type: object + properties: + matchLabels: + type: object + x-kubernetes-preserve-unknown-fields: true + matchExpressions: + type: array + items: + type: object + required: [key, operator] + properties: + key: + type: string + operator: + type: string + enum: [In, NotIn, Exists, DoesNotExist] + values: + type: array + items: + type: string + client: + description: Describes clients authorized to access a server. + type: object + properties: + networks: + description: >- + Limits the client IP addresses to which this + authorization applies. If unset, the server chooses a + default (typically, all IPs or the cluster's pod + network). + type: array + items: + type: object + required: [cidr] + properties: + cidr: + type: string + except: + type: array + items: + type: string + unauthenticated: + description: >- + Authorizes unauthenticated clients to access a server. + type: boolean + meshTLS: + type: object + properties: + unauthenticatedTLS: + type: boolean + description: >- + Indicates that no client identity is required for + communication. + + This is mostly important for the identity + controller, which must terminate TLS connections + from clients that do not yet have a certificate. + identities: + description: >- + Authorizes clients with the provided proxy identity + strings (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + serviceAccounts: + description: >- + Authorizes clients with the provided proxy identity + service accounts (as provided via MTLS) + type: array + items: + type: object + required: [name] + properties: + name: + description: The ServiceAccount's name. + type: string + pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' + namespace: + description: >- + The ServiceAccount's namespace. If unset, the + authorization's namespace is used. + type: string + pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' + - name: v1beta1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied servers. + type: object + required: [server, client] + properties: + server: + description: >- + Identifies servers in the same namespace for which this + authorization applies. + + Only one of `name` or `selector` may be specified. + type: object + oneOf: + - required: [name] + - required: [selector] + properties: + name: + description: References a `Server` instance by name + type: string + pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' + selector: + description: >- + A label query over servers on which this authorization applies. + type: object + properties: + matchLabels: + type: object + x-kubernetes-preserve-unknown-fields: true + matchExpressions: + type: array + items: + type: object + required: [key, operator] + properties: + key: + type: string + operator: + type: string + enum: [In, NotIn, Exists, DoesNotExist] + values: + type: array + items: + type: string + client: + description: Describes clients authorized to access a server. + type: object + properties: + networks: + description: >- + Limits the client IP addresses to which this + authorization applies. If unset, the server chooses a + default (typically, all IPs or the cluster's pod + network). + type: array + items: + type: object + required: [cidr] + properties: + cidr: + type: string + except: + type: array + items: + type: string + unauthenticated: + description: >- + Authorizes unauthenticated clients to access a server. + type: boolean + meshTLS: + type: object + properties: + unauthenticatedTLS: + type: boolean + description: >- + Indicates that no client identity is required for + communication. + + This is mostly important for the identity + controller, which must terminate TLS connections + from clients that do not yet have a certificate. + identities: + description: >- + Authorizes clients with the provided proxy identity + strings (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + serviceAccounts: + description: >- + Authorizes clients with the provided proxy identity + service accounts (as provided via MTLS) + type: array + items: + type: object + required: [name] + properties: + name: + description: The ServiceAccount's name. + type: string + pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' + namespace: + description: >- + The ServiceAccount's namespace. If unset, the + authorization's namespace is used. + type: string + pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' + additionalPrinterColumns: + - name: Server + type: string + description: The server that this grants access to + jsonPath: .spec.server.name diff --git a/linkerd/charts/linkerd-crds/templates/policy/server.yaml b/linkerd/charts/linkerd-crds/templates/policy/server.yaml new file mode 100644 index 0000000..a5d9f26 --- /dev/null +++ b/linkerd/charts/linkerd-crds/templates/policy/server.yaml @@ -0,0 +1,132 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: servers.policy.linkerd.io + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + linkerd.io/control-plane-ns: {{.Release.Namespace}} +spec: + group: policy.linkerd.io + names: + kind: Server + plural: servers + singular: server + shortNames: [srv] + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: false + deprecated: true + deprecationWarning: "policy.linkerd.io/v1alpha1 Server is deprecated; use policy.linkerd.io/v1beta1 Server" + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + type: object + required: + - podSelector + - port + properties: + podSelector: + type: object + description: >- + Selects pods in the same namespace. + oneOf: + - required: [matchExpressions] + - required: [matchLabels] + properties: + matchLabels: + type: object + x-kubernetes-preserve-unknown-fields: true + matchExpressions: + type: array + items: + type: object + required: [key, operator] + properties: + key: + type: string + operator: + type: string + enum: [In, NotIn, Exists, DoesNotExist] + values: + type: array + items: + type: string + port: + description: >- + A port name or number. Must exist in a pod spec. + x-kubernetes-int-or-string: true + proxyProtocol: + description: >- + Configures protocol discovery for inbound connections. + + Supersedes the `config.linkerd.io/opaque-ports` annotation. + type: string + default: unknown + - name: v1beta1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + type: object + required: + - podSelector + - port + properties: + podSelector: + type: object + description: >- + Selects pods in the same namespace. + + The result of matchLabels and matchExpressions are ANDed. + Selects all if empty. + properties: + matchLabels: + type: object + x-kubernetes-preserve-unknown-fields: true + matchExpressions: + type: array + items: + type: object + required: [key, operator] + properties: + key: + type: string + operator: + type: string + enum: [In, NotIn, Exists, DoesNotExist] + values: + type: array + items: + type: string + port: + description: >- + A port name or number. Must exist in a pod spec. + x-kubernetes-int-or-string: true + proxyProtocol: + description: >- + Configures protocol discovery for inbound connections. + + Supersedes the `config.linkerd.io/opaque-ports` annotation. + type: string + default: unknown + additionalPrinterColumns: + - name: Port + type: string + description: The port the server is listening on + jsonPath: .spec.port + - name: Protocol + type: string + description: The protocol of the server + jsonPath: .spec.proxyProtocol diff --git a/linkerd/charts/linkerd-crds/templates/serviceprofile.yaml b/linkerd/charts/linkerd-crds/templates/serviceprofile.yaml new file mode 100644 index 0000000..d1a0bfc --- /dev/null +++ b/linkerd/charts/linkerd-crds/templates/serviceprofile.yaml @@ -0,0 +1,278 @@ +--- +### +### Service Profile CRD +### +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: serviceprofiles.linkerd.io + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + linkerd.io/control-plane-ns: {{.Release.Namespace}} +spec: + group: linkerd.io + versions: + - name: v1alpha1 + served: true + storage: false + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + description: Spec is the custom resource spec + required: + - routes + properties: + dstOverrides: + type: array + required: + - authority + - weight + items: + type: object + description: WeightedDst is a weighted alternate destination. + properties: + authority: + type: string + weight: + x-kubernetes-int-or-string: true + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + opaquePorts: + type: array + items: + type: string + retryBudget: + type: object + required: + - minRetriesPerSecond + - retryRatio + - ttl + description: RetryBudget describes the maximum number of retries that should be issued to this service. + properties: + minRetriesPerSecond: + format: int32 + type: integer + retryRatio: + type: number + format: float + ttl: + type: string + routes: + type: array + items: + type: object + description: RouteSpec specifies a Route resource. + required: + - condition + - name + properties: + condition: + type: object + description: RequestMatch describes the conditions under which to match a Route. + properties: + pathRegex: + type: string + method: + type: string + all: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + any: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + not: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + isRetryable: + type: boolean + name: + type: string + timeout: + type: string + responseClasses: + type: array + items: + type: object + required: + - condition + description: ResponseClass describes how to classify a response (e.g. success or failures). + properties: + condition: + type: object + description: ResponseMatch describes the conditions under + which to classify a response. + properties: + all: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + any: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + not: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + status: + type: object + description: Range describes a range of integers (e.g. status codes). + properties: + max: + format: int32 + type: integer + min: + format: int32 + type: integer + isFailure: + type: boolean + - name: v1alpha2 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + description: Spec is the custom resource spec + properties: + dstOverrides: + type: array + required: + - authority + - weight + items: + type: object + description: WeightedDst is a weighted alternate destination. + properties: + authority: + type: string + weight: + x-kubernetes-int-or-string: true + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + opaquePorts: + type: array + items: + type: string + retryBudget: + type: object + required: + - minRetriesPerSecond + - retryRatio + - ttl + description: RetryBudget describes the maximum number of retries that should be issued to this service. + properties: + minRetriesPerSecond: + format: int32 + type: integer + retryRatio: + type: number + format: float + ttl: + type: string + routes: + type: array + items: + type: object + description: RouteSpec specifies a Route resource. + required: + - condition + - name + properties: + condition: + type: object + description: RequestMatch describes the conditions under which to match a Route. + properties: + pathRegex: + type: string + method: + type: string + all: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + any: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + not: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + isRetryable: + type: boolean + name: + type: string + timeout: + type: string + responseClasses: + type: array + items: + type: object + required: + - condition + description: ResponseClass describes how to classify a response (e.g. success or failures). + properties: + condition: + type: object + description: ResponseMatch describes the conditions under + which to classify a response. + properties: + all: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + any: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + not: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + status: + type: object + description: Range describes a range of integers (e.g. status codes). + properties: + max: + format: int32 + type: integer + min: + format: int32 + type: integer + isFailure: + type: boolean + scope: Namespaced + preserveUnknownFields: false + names: + plural: serviceprofiles + singular: serviceprofile + kind: ServiceProfile + shortNames: + - sp diff --git a/linkerd/charts/linkerd-crds/values.yaml b/linkerd/charts/linkerd-crds/values.yaml new file mode 100644 index 0000000..3621451 --- /dev/null +++ b/linkerd/charts/linkerd-crds/values.yaml @@ -0,0 +1 @@ +enableHttpRoutes: true diff --git a/linkerd/charts/linkerd-viz/.helmignore b/linkerd/charts/linkerd-viz/.helmignore new file mode 100644 index 0000000..79c90a8 --- /dev/null +++ b/linkerd/charts/linkerd-viz/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +OWNERS +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/linkerd/charts/linkerd-viz/Chart.yaml b/linkerd/charts/linkerd-viz/Chart.yaml new file mode 100644 index 0000000..22e1f4e --- /dev/null +++ b/linkerd/charts/linkerd-viz/Chart.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +appVersion: stable-2.14.10 +description: 'The Linkerd-Viz extension contains observability and visualization components + for Linkerd. ' +home: https://linkerd.io +icon: https://linkerd.io/images/logo-only-200h.png +keywords: +- service-mesh +kubeVersion: '>=1.21.0-0' +maintainers: +- email: cncf-linkerd-dev@lists.cncf.io + name: Linkerd authors + url: https://linkerd.io/ +name: linkerd-viz +sources: +- https://github.com/linkerd/linkerd2/ +version: 30.12.11 diff --git a/linkerd/charts/linkerd-viz/README.md b/linkerd/charts/linkerd-viz/README.md new file mode 100644 index 0000000..fd1b5e4 --- /dev/null +++ b/linkerd/charts/linkerd-viz/README.md @@ -0,0 +1,214 @@ +# linkerd-viz + +The Linkerd-Viz extension contains observability and visualization +components for Linkerd. + +![Version: 30.12.11](https://img.shields.io/badge/Version-30.12.11-informational?style=flat-square) + +![AppVersion: edge-XX.X.X](https://img.shields.io/badge/AppVersion-edge--XX.X.X-informational?style=flat-square) + +**Homepage:** + +## Quickstart and documentation + +You can run Linkerd on any Kubernetes 1.21+ cluster in a matter of seconds. See +the [Linkerd Getting Started Guide][getting-started] for how. + +For more comprehensive documentation, start with the [Linkerd +docs][linkerd-docs]. + +## Prerequisite: Linkerd Core Control-Plane + +Before installing the Linkerd Viz extension, The core control-plane has to +be installed first by following the [Linkerd Install +Guide](https://linkerd.io/2/tasks/install/). + +## Adding Linkerd's Helm repository + +```bash +# To add the repo for Linkerd stable releases: +helm repo add linkerd https://helm.linkerd.io/stable +# To add the repo for Linkerd edge releases: +helm repo add linkerd-edge https://helm.linkerd.io/edge +``` + +The following instructions use the `linkerd` repo. For installing an edge +release, just replace with `linkerd-edge`. + +## Installing the Viz Extension Chart + +```bash +helm install linkerd-viz -n linkerd-viz --create-namespace linkerd/linkerd-viz +``` + +## Get involved + +* Check out Linkerd's source code at [GitHub][linkerd2]. +* Join Linkerd's [user mailing list][linkerd-users], [developer mailing + list][linkerd-dev], and [announcements mailing list][linkerd-announce]. +* Follow [@linkerd][twitter] on Twitter. +* Join the [Linkerd Slack][slack]. + +[getting-started]: https://linkerd.io/2/getting-started/ +[linkerd2]: https://github.com/linkerd/linkerd2 +[linkerd-announce]: https://lists.cncf.io/g/cncf-linkerd-announce +[linkerd-dev]: https://lists.cncf.io/g/cncf-linkerd-dev +[linkerd-docs]: https://linkerd.io/2/overview/ +[linkerd-users]: https://lists.cncf.io/g/cncf-linkerd-users +[slack]: http://slack.linkerd.io +[twitter]: https://twitter.com/linkerd + +## Requirements + +Kubernetes: `>=1.21.0-0` + +| Repository | Name | Version | +|------------|------|---------| +| file://../../../charts/partials | partials | 0.1.0 | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| clusterDomain | string | `"cluster.local"` | Kubernetes DNS Domain name to use | +| commonLabels | object | `{}` | Labels to apply to all resources | +| dashboard.UID | string | `nil` | UID for the dashboard resource | +| dashboard.enforcedHostRegexp | string | `""` | Host header validation regex for the dashboard. See the [Linkerd documentation](https://linkerd.io/2/tasks/exposing-dashboard) for more information | +| dashboard.image.name | string | `"web"` | Docker image name for the web instance | +| dashboard.image.pullPolicy | string | defaultImagePullPolicy | Pull policy for the web component | +| dashboard.image.registry | string | defaultRegistry | Docker registry for the web instance | +| dashboard.image.tag | string | linkerdVersion | Docker image tag for the web instance | +| dashboard.logFormat | string | defaultLogFormat | log format of the dashboard component | +| dashboard.logLevel | string | defaultLogLevel | log level of the dashboard component | +| dashboard.proxy | string | `nil` | | +| dashboard.replicas | int | `1` | Number of replicas of dashboard | +| dashboard.resources.cpu.limit | string | `nil` | Maximum amount of CPU units that the web container can use | +| dashboard.resources.cpu.request | string | `nil` | Amount of CPU units that the web container requests | +| dashboard.resources.ephemeral-storage.limit | string | `""` | Maximum amount of ephemeral storage that the web container can use | +| dashboard.resources.ephemeral-storage.request | string | `""` | Amount of ephemeral storage that the web container requests | +| dashboard.resources.memory.limit | string | `nil` | Maximum amount of memory that web container can use | +| dashboard.resources.memory.request | string | `nil` | Amount of memory that the web container requests | +| dashboard.restrictPrivileges | bool | `false` | Restrict the Linkerd Dashboard's default privileges to disallow Tap and Check | +| dashboard.service | object | `{"annotations":{}}` | dashboard service configuration | +| dashboard.service.annotations | object | `{}` | Additional annotations to add to dashboard service | +| defaultImagePullPolicy | string | `"IfNotPresent"` | Docker imagePullPolicy for all viz components | +| defaultLogFormat | string | `"plain"` | Log format (`plain` or `json`) for all the viz components. | +| defaultLogLevel | string | `"info"` | Log level for all the viz components | +| defaultRegistry | string | `"cr.l5d.io/linkerd"` | Docker registry for all viz components | +| defaultUID | int | `2103` | UID for all the viz components | +| enablePSP | bool | `false` | Create Roles and RoleBindings to associate this extension's ServiceAccounts to the control plane PSP resource. This requires that `enabledPSP` is set to true on the control plane install. Note PSP has been deprecated since k8s v1.21 | +| enablePodAntiAffinity | bool | `false` | Enables Pod Anti Affinity logic to balance the placement of replicas across hosts and zones for High Availability. Enable this only when you have multiple replicas of components. | +| grafana.externalUrl | string | `nil` | url of a Grafana instance hosted off-cluster. Cannot be set if grafana.url is set. The reverse proxy will not be used for this URL. | +| grafana.uidPrefix | string | `nil` | prefix for Grafana dashboard UID's, used when grafana.externalUrl is set. | +| grafana.url | string | `nil` | url of an in-cluster Grafana instance with reverse proxy configured, used by the Linkerd viz web dashboard to provide direct links to specific Grafana dashboards. Cannot be set if grafana.externalUrl is set. See the [Linkerd documentation](https://linkerd.io/2/tasks/grafana) for more information | +| identityTrustDomain | string | clusterDomain | Trust domain used for identity | +| imagePullSecrets | list | `[]` | For Private docker registries, authentication is needed. Registry secrets are applied to the respective service accounts | +| jaegerUrl | string | `""` | url of external jaeger instance Set this to `jaeger.linkerd-jaeger.svc.:16686` if you plan to use jaeger extension | +| linkerdNamespace | string | `"linkerd"` | Namespace of the Linkerd core control-plane install | +| linkerdVersion | string | `"linkerdVersionValue"` | control plane version. See Proxy section for proxy version | +| metricsAPI.UID | string | `nil` | UID for the metrics-api resource | +| metricsAPI.image.name | string | `"metrics-api"` | Docker image name for the metrics-api component | +| metricsAPI.image.pullPolicy | string | defaultImagePullPolicy | Pull policy for the metrics-api component | +| metricsAPI.image.registry | string | defaultRegistry | Docker registry for the metrics-api component | +| metricsAPI.image.tag | string | linkerdVersion | Docker image tag for the metrics-api component | +| metricsAPI.logFormat | string | defaultLogFormat | log format of the metrics-api component | +| metricsAPI.logLevel | string | defaultLogLevel | log level of the metrics-api component | +| metricsAPI.nodeSelector | object | `{"kubernetes.io/os":"linux"}` | NodeSelector section, See the [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information | +| metricsAPI.proxy | string | `nil` | | +| metricsAPI.replicas | int | `1` | number of replicas of the metrics-api component | +| metricsAPI.resources.cpu.limit | string | `nil` | Maximum amount of CPU units that the metrics-api container can use | +| metricsAPI.resources.cpu.request | string | `nil` | Amount of CPU units that the metrics-api container requests | +| metricsAPI.resources.ephemeral-storage.limit | string | `""` | Maximum amount of ephemeral storage that the metrics-api container can use | +| metricsAPI.resources.ephemeral-storage.request | string | `""` | Amount of ephemeral storage that the metrics-api container requests | +| metricsAPI.resources.memory.limit | string | `nil` | Maximum amount of memory that metrics-api container can use | +| metricsAPI.resources.memory.request | string | `nil` | Amount of memory that the metrics-api container requests | +| metricsAPI.service | object | `{"annotations":{}}` | metrics-api service configuration | +| metricsAPI.service.annotations | object | `{}` | Additional annotations to add to metrics-api service | +| metricsAPI.tolerations | string | `nil` | Tolerations section, See the [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for more information | +| namespaceMetadata.image.name | string | `"extension-init"` | Docker image name for the namespace-metadata instance | +| namespaceMetadata.image.pullPolicy | string | defaultImagePullPolicy | Pull policy for the namespace-metadata instance | +| namespaceMetadata.image.registry | string | defaultRegistry | Docker registry for the namespace-metadata instance | +| namespaceMetadata.image.tag | string | `"v0.1.0"` | Docker image tag for the namespace-metadata instance | +| namespaceMetadata.nodeSelector | object | `{"kubernetes.io/os":"linux"}` | NodeSelector section, See the [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information | +| namespaceMetadata.tolerations | string | `nil` | Tolerations section, See the [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for more information | +| nodeSelector | object | `{"kubernetes.io/os":"linux"}` | Default nodeSelector section, See the [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information | +| podLabels | object | `{}` | Additional labels to add to all pods | +| prometheus.alertRelabelConfigs | string | `nil` | Alert relabeling is applied to alerts before they are sent to the Alertmanager. | +| prometheus.alertmanagers | string | `nil` | Alertmanager instances the Prometheus server sends alerts to configured via the static_configs parameter. | +| prometheus.args | object | `{"config.file":"/etc/prometheus/prometheus.yml","storage.tsdb.path":"/data","storage.tsdb.retention.time":"6h"}` | Command line options for Prometheus binary | +| prometheus.enabled | bool | `true` | toggle field to enable or disable prometheus | +| prometheus.globalConfig | object | `{"evaluation_interval":"10s","scrape_interval":"10s","scrape_timeout":"10s"}` | The global configuration specifies parameters that are valid in all other configuration contexts. | +| prometheus.image.name | string | `"prometheus"` | Docker image name for the prometheus instance | +| prometheus.image.pullPolicy | string | defaultImagePullPolicy | Pull policy for the prometheus instance | +| prometheus.image.registry | string | `"prom"` | Docker registry for the prometheus instance | +| prometheus.image.tag | string | `"v2.48.0"` | Docker image tag for the prometheus instance | +| prometheus.logFormat | string | defaultLogLevel | log format (plain, json) of the prometheus instance | +| prometheus.logLevel | string | defaultLogLevel | log level of the prometheus instance | +| prometheus.nodeSelector | object | `{"kubernetes.io/os":"linux"}` | NodeSelector section, See the [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information | +| prometheus.proxy | string | `nil` | | +| prometheus.remoteWrite | string | `nil` | Allows transparently sending samples to an endpoint. Mostly used for long term storage. | +| prometheus.resources.cpu.limit | string | `nil` | Maximum amount of CPU units that the prometheus container can use | +| prometheus.resources.cpu.request | string | `nil` | Amount of CPU units that the prometheus container requests | +| prometheus.resources.ephemeral-storage.limit | string | `""` | Maximum amount of ephemeral storage that the prometheus container can use | +| prometheus.resources.ephemeral-storage.request | string | `""` | Amount of ephemeral storage that the prometheus container requests | +| prometheus.resources.memory.limit | string | `nil` | Maximum amount of memory that prometheus container can use | +| prometheus.resources.memory.request | string | `nil` | Amount of memory that the prometheus container requests | +| prometheus.ruleConfigMapMounts | string | `nil` | Alerting/recording rule ConfigMap mounts (sub-path names must end in ´_rules.yml´ or ´_rules.yaml´) | +| prometheus.scrapeConfigs | string | `nil` | A scrapeConfigs section specifies a set of targets and parameters describing how to scrape them. | +| prometheus.sidecarContainers | string | `nil` | A sidecarContainers section specifies a list of secondary containers to run in the prometheus pod e.g. to export data to non-prometheus systems | +| prometheus.tolerations | string | `nil` | Tolerations section, See the [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for more information | +| prometheusUrl | string | `""` | url of external prometheus instance | +| tap.UID | string | `nil` | UID for the dashboard resource | +| tap.caBundle | string | `""` | Bundle of CA certificates for tap. If not provided nor injected with cert-manager, then Helm will use the certificate generated for `tap.crtPEM`. If `tap.externalSecret` is set to true, this value, injectCaFrom, or injectCaFromSecret must be set, as no certificate will be generated. See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector) for more information. | +| tap.crtPEM | string | `""` | Certificate for the Tap component. If not provided and not using an external secret then Helm will generate one. | +| tap.externalSecret | bool | `false` | Do not create a secret resource for the Tap component. If this is set to `true`, the value `tap.caBundle` must be set or the ca bundle must injected with cert-manager ca injector using `tap.injectCaFrom` or `tap.injectCaFromSecret` (see below). | +| tap.ignoreHeaders | list | `[]` | List of headers that will be ignored for Linkerd Tap | +| tap.image.name | string | `"tap"` | Docker image name for the tap instance | +| tap.image.pullPolicy | string | defaultImagePullPolicy | Pull policy for the tap component | +| tap.image.registry | string | defaultRegistry | Docker registry for the tap instance | +| tap.image.tag | string | linkerdVersion | Docker image tag for the tap instance | +| tap.injectCaFrom | string | `""` | Inject the CA bundle from a cert-manager Certificate. See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-certificate-resource) for more information. | +| tap.injectCaFromSecret | string | `""` | Inject the CA bundle from a Secret. If set, the `cert-manager.io/inject-ca-from-secret` annotation will be added to the webhook. The Secret must have the CA Bundle stored in the `ca.crt` key and have the `cert-manager.io/allow-direct-injection` annotation set to `true`. See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-secret-resource) for more information. | +| tap.keyPEM | string | `""` | Certificate key for Tap component. If not provided and not using an external secret then Helm will generate one. | +| tap.logFormat | string | defaultLogFormat | log format of the tap component | +| tap.logLevel | string | defaultLogLevel | log level of the tap component | +| tap.proxy | string | `nil` | | +| tap.replicas | int | `1` | Number of tap component replicas | +| tap.resources.cpu.limit | string | `nil` | Maximum amount of CPU units that the tap container can use | +| tap.resources.cpu.request | string | `nil` | Amount of CPU units that the tap container requests | +| tap.resources.ephemeral-storage.limit | string | `""` | Maximum amount of ephemeral storage that the tap container can use | +| tap.resources.ephemeral-storage.request | string | `""` | Amount of ephemeral storage that the tap container requests | +| tap.resources.memory.limit | string | `nil` | Maximum amount of memory that tap container can use | +| tap.resources.memory.request | string | `nil` | Amount of memory that the tap container requests | +| tap.service | object | `{"annotations":{}}` | tap service configuration | +| tap.service.annotations | object | `{}` | Additional annotations to add to tap service | +| tapInjector.UID | string | `nil` | UID for the tapInjector resource | +| tapInjector.caBundle | string | `""` | Bundle of CA certificates for the tapInjector. If not provided nor injected with cert-manager, then Helm will use the certificate generated for `tapInjector.crtPEM`. If `tapInjector.externalSecret` is set to true, this value, injectCaFrom, or injectCaFromSecret must be set, as no certificate will be generated. See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector) for more information. | +| tapInjector.crtPEM | string | `""` | Certificate for the tapInjector. If not provided and not using an external secret then Helm will generate one. | +| tapInjector.externalSecret | bool | `false` | Do not create a secret resource for the tapInjector webhook. If this is set to `true`, the value `tapInjector.caBundle` must be set or the ca bundle must injected with cert-manager ca injector using `tapInjector.injectCaFrom` or `tapInjector.injectCaFromSecret` (see below). | +| tapInjector.failurePolicy | string | `"Ignore"` | | +| tapInjector.image.name | string | `"tap"` | Docker image name for the tapInjector instance | +| tapInjector.image.pullPolicy | string | defaultImagePullPolicy | Pull policy for the tapInjector component | +| tapInjector.image.registry | string | defaultRegistry | Docker registry for the tapInjector instance | +| tapInjector.image.tag | string | linkerdVersion | Docker image tag for the tapInjector instance | +| tapInjector.injectCaFrom | string | `""` | Inject the CA bundle from a cert-manager Certificate. See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-certificate-resource) for more information. | +| tapInjector.injectCaFromSecret | string | `""` | Inject the CA bundle from a Secret. If set, the `cert-manager.io/inject-ca-from-secret` annotation will be added to the webhook. The Secret must have the CA Bundle stored in the `ca.crt` key and have the `cert-manager.io/allow-direct-injection` annotation set to `true`. See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-secret-resource) for more information. | +| tapInjector.keyPEM | string | `""` | Certificate key for the tapInjector. If not provided and not using an external secret then Helm will generate one. | +| tapInjector.logFormat | string | defaultLogFormat | log format of the tapInjector component | +| tapInjector.logLevel | string | defaultLogLevel | log level of the tapInjector | +| tapInjector.namespaceSelector | string | `nil` | | +| tapInjector.objectSelector | string | `nil` | | +| tapInjector.proxy | string | `nil` | | +| tapInjector.replicas | int | `1` | Number of replicas of tapInjector | +| tapInjector.resources.cpu.limit | string | `nil` | Maximum amount of CPU units that the tapInjector container can use | +| tapInjector.resources.cpu.request | string | `nil` | Amount of CPU units that the tapInjector container requests | +| tapInjector.resources.ephemeral-storage.limit | string | `""` | Maximum amount of ephemeral storage that the tapInjector container can use | +| tapInjector.resources.ephemeral-storage.request | string | `""` | Amount of ephemeral storage that the tapInjector container requests | +| tapInjector.resources.memory.limit | string | `nil` | Maximum amount of memory that tapInjector container can use | +| tapInjector.resources.memory.request | string | `nil` | Amount of memory that the tapInjector container requests | +| tapInjector.service | object | `{"annotations":{}}` | tap service configuration | +| tapInjector.service.annotations | object | `{}` | Additional annotations to add to tapInjector service | +| tolerations | string | `nil` | Default tolerations section, See the [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for more information | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/linkerd/charts/linkerd-viz/README.md.gotmpl b/linkerd/charts/linkerd-viz/README.md.gotmpl new file mode 100644 index 0000000..72f7e7c --- /dev/null +++ b/linkerd/charts/linkerd-viz/README.md.gotmpl @@ -0,0 +1,63 @@ +{{ template "chart.header" . }} +{{ template "chart.description" . }} + +{{ template "chart.versionBadge" . }} +{{ template "chart.typeBadge" . }} +{{ template "chart.appVersionBadge" . }} + +{{ template "chart.homepageLine" . }} + +## Quickstart and documentation + +You can run Linkerd on any Kubernetes 1.21+ cluster in a matter of seconds. See +the [Linkerd Getting Started Guide][getting-started] for how. + +For more comprehensive documentation, start with the [Linkerd +docs][linkerd-docs]. + +## Prerequisite: Linkerd Core Control-Plane + +Before installing the Linkerd Viz extension, The core control-plane has to +be installed first by following the [Linkerd Install +Guide](https://linkerd.io/2/tasks/install/). + +## Adding Linkerd's Helm repository + +```bash +# To add the repo for Linkerd stable releases: +helm repo add linkerd https://helm.linkerd.io/stable +# To add the repo for Linkerd edge releases: +helm repo add linkerd-edge https://helm.linkerd.io/edge +``` + +The following instructions use the `linkerd` repo. For installing an edge +release, just replace with `linkerd-edge`. + +## Installing the Viz Extension Chart + +```bash +helm install linkerd-viz -n linkerd-viz --create-namespace linkerd/linkerd-viz +``` + +## Get involved + +* Check out Linkerd's source code at [GitHub][linkerd2]. +* Join Linkerd's [user mailing list][linkerd-users], [developer mailing + list][linkerd-dev], and [announcements mailing list][linkerd-announce]. +* Follow [@linkerd][twitter] on Twitter. +* Join the [Linkerd Slack][slack]. + +[getting-started]: https://linkerd.io/2/getting-started/ +[linkerd2]: https://github.com/linkerd/linkerd2 +[linkerd-announce]: https://lists.cncf.io/g/cncf-linkerd-announce +[linkerd-dev]: https://lists.cncf.io/g/cncf-linkerd-dev +[linkerd-docs]: https://linkerd.io/2/overview/ +[linkerd-users]: https://lists.cncf.io/g/cncf-linkerd-users +[slack]: http://slack.linkerd.io +[twitter]: https://twitter.com/linkerd + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/linkerd/charts/linkerd-viz/charts/partials/.helmignore b/linkerd/charts/linkerd-viz/charts/partials/.helmignore new file mode 100644 index 0000000..f0c1319 --- /dev/null +++ b/linkerd/charts/linkerd-viz/charts/partials/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/linkerd/charts/linkerd-viz/charts/partials/Chart.yaml b/linkerd/charts/linkerd-viz/charts/partials/Chart.yaml new file mode 100644 index 0000000..23cfc16 --- /dev/null +++ b/linkerd/charts/linkerd-viz/charts/partials/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +description: 'A Helm chart containing Linkerd partial templates, depended by the ''linkerd'' + and ''patch'' charts. ' +name: partials +version: 0.1.0 diff --git a/linkerd/charts/linkerd-viz/charts/partials/README.md b/linkerd/charts/linkerd-viz/charts/partials/README.md new file mode 100644 index 0000000..e5fbd39 --- /dev/null +++ b/linkerd/charts/linkerd-viz/charts/partials/README.md @@ -0,0 +1,9 @@ +# partials + +A Helm chart containing Linkerd partial templates, +depended by the 'linkerd' and 'patch' charts. + +![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/linkerd/charts/linkerd-viz/charts/partials/README.md.gotmpl b/linkerd/charts/linkerd-viz/charts/partials/README.md.gotmpl new file mode 100644 index 0000000..37f5101 --- /dev/null +++ b/linkerd/charts/linkerd-viz/charts/partials/README.md.gotmpl @@ -0,0 +1,14 @@ +{{ template "chart.header" . }} +{{ template "chart.description" . }} + +{{ template "chart.versionBadge" . }} +{{ template "chart.typeBadge" . }} +{{ template "chart.appVersionBadge" . }} + +{{ template "chart.homepageLine" . }} + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/linkerd/charts/linkerd-viz/charts/partials/templates/NOTES.txt b/linkerd/charts/linkerd-viz/charts/partials/templates/NOTES.txt new file mode 100644 index 0000000..e69de29 diff --git a/linkerd/charts/linkerd-viz/charts/partials/templates/_affinity.tpl b/linkerd/charts/linkerd-viz/charts/partials/templates/_affinity.tpl new file mode 100644 index 0000000..5dde1da --- /dev/null +++ b/linkerd/charts/linkerd-viz/charts/partials/templates/_affinity.tpl @@ -0,0 +1,38 @@ +{{ define "linkerd.pod-affinity" -}} +podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: {{ default "linkerd.io/control-plane-component" .label }} + operator: In + values: + - {{ .component }} + topologyKey: topology.kubernetes.io/zone + weight: 100 + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: {{ default "linkerd.io/control-plane-component" .label }} + operator: In + values: + - {{ .component }} + topologyKey: kubernetes.io/hostname +{{- end }} + +{{ define "linkerd.node-affinity" -}} +nodeAffinity: +{{- toYaml .Values.nodeAffinity | trim | nindent 2 }} +{{- end }} + +{{ define "linkerd.affinity" -}} +{{- if or .Values.enablePodAntiAffinity .Values.nodeAffinity -}} +affinity: +{{- end }} +{{- if .Values.enablePodAntiAffinity -}} +{{- include "linkerd.pod-affinity" . | nindent 2 }} +{{- end }} +{{- if .Values.nodeAffinity -}} +{{- include "linkerd.node-affinity" . | nindent 2 }} +{{- end }} +{{- end }} diff --git a/linkerd/charts/linkerd-viz/charts/partials/templates/_capabilities.tpl b/linkerd/charts/linkerd-viz/charts/partials/templates/_capabilities.tpl new file mode 100644 index 0000000..a595d74 --- /dev/null +++ b/linkerd/charts/linkerd-viz/charts/partials/templates/_capabilities.tpl @@ -0,0 +1,16 @@ +{{- define "partials.proxy.capabilities" -}} +capabilities: + {{- if .Values.proxy.capabilities.add }} + add: + {{- toYaml .Values.proxy.capabilities.add | trim | nindent 4 }} + {{- end }} + {{- if .Values.proxy.capabilities.drop }} + drop: + {{- toYaml .Values.proxy.capabilities.drop | trim | nindent 4 }} + {{- end }} +{{- end -}} + +{{- define "partials.proxy-init.capabilities.drop" -}} +drop: +{{ toYaml .Values.proxyInit.capabilities.drop | trim }} +{{- end -}} diff --git a/linkerd/charts/linkerd-viz/charts/partials/templates/_debug.tpl b/linkerd/charts/linkerd-viz/charts/partials/templates/_debug.tpl new file mode 100644 index 0000000..cf0eb14 --- /dev/null +++ b/linkerd/charts/linkerd-viz/charts/partials/templates/_debug.tpl @@ -0,0 +1,6 @@ +{{- define "partials.debug" -}} +image: {{.Values.debugContainer.image.name}}:{{.Values.debugContainer.image.version | default .Values.linkerdVersion}} +imagePullPolicy: {{.Values.debugContainer.image.pullPolicy | default .Values.imagePullPolicy}} +name: linkerd-debug +terminationMessagePolicy: FallbackToLogsOnError +{{- end -}} diff --git a/linkerd/charts/linkerd-viz/charts/partials/templates/_helpers.tpl b/linkerd/charts/linkerd-viz/charts/partials/templates/_helpers.tpl new file mode 100644 index 0000000..b6cdc34 --- /dev/null +++ b/linkerd/charts/linkerd-viz/charts/partials/templates/_helpers.tpl @@ -0,0 +1,14 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Splits a coma separated list into a list of string values. +For example "11,22,55,44" will become "11","22","55","44" +*/}} +{{- define "partials.splitStringList" -}} +{{- if gt (len (toString .)) 0 -}} +{{- $ports := toString . | splitList "," -}} +{{- $last := sub (len $ports) 1 -}} +{{- range $i,$port := $ports -}} +"{{$port}}"{{ternary "," "" (ne $i $last)}} +{{- end -}} +{{- end -}} +{{- end -}} diff --git a/linkerd/charts/linkerd-viz/charts/partials/templates/_metadata.tpl b/linkerd/charts/linkerd-viz/charts/partials/templates/_metadata.tpl new file mode 100644 index 0000000..04d2f1b --- /dev/null +++ b/linkerd/charts/linkerd-viz/charts/partials/templates/_metadata.tpl @@ -0,0 +1,17 @@ +{{- define "partials.annotations.created-by" -}} +linkerd.io/created-by: {{ .Values.cliVersion | default (printf "linkerd/helm %s" ( (.Values.image).version | default .Values.linkerdVersion)) }} +{{- end -}} + +{{- define "partials.proxy.annotations" -}} +linkerd.io/proxy-version: {{.Values.proxy.image.version | default .Values.linkerdVersion}} +cluster-autoscaler.kubernetes.io/safe-to-evict: "true" +linkerd.io/trust-root-sha256: {{ .Values.identityTrustAnchorsPEM | sha256sum }} +{{- end -}} + +{{/* +To add labels to the control-plane components, instead update at individual component manifests as +adding here would also update `spec.selector.matchLabels` which are immutable and would fail upgrades. +*/}} +{{- define "partials.proxy.labels" -}} +linkerd.io/proxy-{{.workloadKind}}: {{.component}} +{{- end -}} diff --git a/linkerd/charts/linkerd-viz/charts/partials/templates/_network-validator.tpl b/linkerd/charts/linkerd-viz/charts/partials/templates/_network-validator.tpl new file mode 100644 index 0000000..58f36e6 --- /dev/null +++ b/linkerd/charts/linkerd-viz/charts/partials/templates/_network-validator.tpl @@ -0,0 +1,32 @@ +{{- define "partials.network-validator" -}} +name: linkerd-network-validator +image: {{.Values.proxy.image.name}}:{{.Values.proxy.image.version | default .Values.linkerdVersion }} +imagePullPolicy: {{.Values.proxy.image.pullPolicy | default .Values.imagePullPolicy}} +{{ include "partials.resources" .Values.proxyInit.resources }} +{{- if or .Values.networkValidator.enableSecurityContext }} +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 65534 + seccompProfile: + type: RuntimeDefault +{{- end }} +command: + - /usr/lib/linkerd/linkerd2-network-validator +args: + - --log-format + - {{ .Values.networkValidator.logFormat }} + - --log-level + - {{ .Values.networkValidator.logLevel }} + - --connect-addr + - {{ .Values.networkValidator.connectAddr }} + - --listen-addr + - {{ .Values.networkValidator.listenAddr }} + - --timeout + - {{ .Values.networkValidator.timeout }} + +{{- end -}} diff --git a/linkerd/charts/linkerd-viz/charts/partials/templates/_nodeselector.tpl b/linkerd/charts/linkerd-viz/charts/partials/templates/_nodeselector.tpl new file mode 100644 index 0000000..4cde0ab --- /dev/null +++ b/linkerd/charts/linkerd-viz/charts/partials/templates/_nodeselector.tpl @@ -0,0 +1,4 @@ +{{- define "linkerd.node-selector" -}} +nodeSelector: +{{- toYaml .Values.nodeSelector | trim | nindent 2 }} +{{- end -}} diff --git a/linkerd/charts/linkerd-viz/charts/partials/templates/_proxy-config-ann.tpl b/linkerd/charts/linkerd-viz/charts/partials/templates/_proxy-config-ann.tpl new file mode 100644 index 0000000..9651b3b --- /dev/null +++ b/linkerd/charts/linkerd-viz/charts/partials/templates/_proxy-config-ann.tpl @@ -0,0 +1,18 @@ +{{- define "partials.proxy.config.annotations" -}} +{{- with .cpu }} +{{- with .request -}} +config.linkerd.io/proxy-cpu-request: {{. | quote}} +{{end}} +{{- with .limit -}} +config.linkerd.io/proxy-cpu-limit: {{. | quote}} +{{- end}} +{{- end}} +{{- with .memory }} +{{- with .request }} +config.linkerd.io/proxy-memory-request: {{. | quote}} +{{end}} +{{- with .limit -}} +config.linkerd.io/proxy-memory-limit: {{. | quote}} +{{- end}} +{{- end }} +{{- end }} diff --git a/linkerd/charts/linkerd-viz/charts/partials/templates/_proxy-init.tpl b/linkerd/charts/linkerd-viz/charts/partials/templates/_proxy-init.tpl new file mode 100644 index 0000000..91cc96e --- /dev/null +++ b/linkerd/charts/linkerd-viz/charts/partials/templates/_proxy-init.tpl @@ -0,0 +1,89 @@ +{{- define "partials.proxy-init" -}} +args: +{{- if (.Values.proxyInit.iptablesMode | default "legacy" | eq "nft") }} +- --firewall-bin-path +- "iptables-nft" +- --firewall-save-bin-path +- "iptables-nft-save" +{{- else if not (eq .Values.proxyInit.iptablesMode "legacy") }} +{{ fail (printf "Unsupported value \"%s\" for proxyInit.iptablesMode\nValid values: [\"nft\", \"legacy\"]" .Values.proxyInit.iptablesMode) }} +{{- end }} +- --incoming-proxy-port +- {{.Values.proxy.ports.inbound | quote}} +- --outgoing-proxy-port +- {{.Values.proxy.ports.outbound | quote}} +- --proxy-uid +- {{.Values.proxy.uid | quote}} +- --inbound-ports-to-ignore +- "{{.Values.proxy.ports.control}},{{.Values.proxy.ports.admin}}{{ternary (printf ",%s" (.Values.proxyInit.ignoreInboundPorts | toString)) "" (not (empty .Values.proxyInit.ignoreInboundPorts)) }}" +{{- if .Values.proxyInit.ignoreOutboundPorts }} +- --outbound-ports-to-ignore +- {{.Values.proxyInit.ignoreOutboundPorts | quote}} +{{- end }} +{{- if .Values.proxyInit.closeWaitTimeoutSecs }} +- --timeout-close-wait-secs +- {{ .Values.proxyInit.closeWaitTimeoutSecs | quote}} +{{- end }} +{{- if .Values.proxyInit.logFormat }} +- --log-format +- {{ .Values.proxyInit.logFormat }} +{{- end }} +{{- if .Values.proxyInit.logLevel }} +- --log-level +- {{ .Values.proxyInit.logLevel }} +{{- end }} +{{- if .Values.proxyInit.skipSubnets }} +- --subnets-to-ignore +- {{ .Values.proxyInit.skipSubnets | quote }} +{{- end }} +image: {{.Values.proxyInit.image.name}}:{{.Values.proxyInit.image.version}} +imagePullPolicy: {{.Values.proxyInit.image.pullPolicy | default .Values.imagePullPolicy}} +name: linkerd-init +{{ include "partials.resources" .Values.proxyInit.resources }} +securityContext: + {{- if or .Values.proxyInit.closeWaitTimeoutSecs .Values.proxyInit.privileged }} + allowPrivilegeEscalation: true + {{- else }} + allowPrivilegeEscalation: false + {{- end }} + capabilities: + add: + - NET_ADMIN + - NET_RAW + {{- if .Values.proxyInit.capabilities -}} + {{- if .Values.proxyInit.capabilities.add }} + {{- toYaml .Values.proxyInit.capabilities.add | trim | nindent 4 }} + {{- end }} + {{- if .Values.proxyInit.capabilities.drop -}} + {{- include "partials.proxy-init.capabilities.drop" . | nindent 4 -}} + {{- end }} + {{- end }} + {{- if or .Values.proxyInit.closeWaitTimeoutSecs .Values.proxyInit.privileged }} + privileged: true + {{- else }} + privileged: false + {{- end }} + {{- if .Values.proxyInit.runAsRoot }} + runAsNonRoot: false + runAsUser: 0 + {{- else }} + runAsNonRoot: true + runAsUser: {{ .Values.proxyInit.runAsUser | int | eq 0 | ternary 65534 .Values.proxyInit.runAsUser }} + {{- end }} + readOnlyRootFilesystem: true + seccompProfile: + type: RuntimeDefault +terminationMessagePolicy: FallbackToLogsOnError +{{- if or (not .Values.cniEnabled) .Values.proxyInit.saMountPath }} +volumeMounts: +{{- end -}} +{{- if not .Values.cniEnabled }} +- mountPath: {{.Values.proxyInit.xtMountPath.mountPath}} + name: {{.Values.proxyInit.xtMountPath.name}} +{{- end -}} +{{- if .Values.proxyInit.saMountPath }} +- mountPath: {{.Values.proxyInit.saMountPath.mountPath}} + name: {{.Values.proxyInit.saMountPath.name}} + readOnly: {{.Values.proxyInit.saMountPath.readOnly}} +{{- end -}} +{{- end -}} diff --git a/linkerd/charts/linkerd-viz/charts/partials/templates/_proxy.tpl b/linkerd/charts/linkerd-viz/charts/partials/templates/_proxy.tpl new file mode 100644 index 0000000..f5dd4c2 --- /dev/null +++ b/linkerd/charts/linkerd-viz/charts/partials/templates/_proxy.tpl @@ -0,0 +1,215 @@ +{{ define "partials.proxy" -}} +{{- $trustDomain := (.Values.identityTrustDomain | default .Values.clusterDomain) -}} +env: +- name: _pod_name + valueFrom: + fieldRef: + fieldPath: metadata.name +- name: _pod_ns + valueFrom: + fieldRef: + fieldPath: metadata.namespace +- name: _pod_nodeName + valueFrom: + fieldRef: + fieldPath: spec.nodeName +{{- if .Values.proxy.cores }} +- name: LINKERD2_PROXY_CORES + value: {{.Values.proxy.cores | quote}} +{{- end }} +{{ if .Values.proxy.requireIdentityOnInboundPorts -}} +- name: LINKERD2_PROXY_INBOUND_PORTS_REQUIRE_IDENTITY + value: {{.Values.proxy.requireIdentityOnInboundPorts | quote}} +{{ end -}} +{{ if .Values.proxy.requireTLSOnInboundPorts -}} +- name: LINKERD2_PROXY_INBOUND_PORTS_REQUIRE_TLS + value: {{.Values.proxy.requireTLSOnInboundPorts | quote}} +{{ end -}} +- name: LINKERD2_PROXY_LOG + value: {{.Values.proxy.logLevel | quote}} +- name: LINKERD2_PROXY_LOG_FORMAT + value: {{.Values.proxy.logFormat | quote}} +- name: LINKERD2_PROXY_DESTINATION_SVC_ADDR + value: {{ternary "localhost.:8086" (printf "linkerd-dst-headless.%s.svc.%s.:8086" .Release.Namespace .Values.clusterDomain) (eq (toString .Values.proxy.component) "linkerd-destination")}} +- name: LINKERD2_PROXY_DESTINATION_PROFILE_NETWORKS + value: {{.Values.clusterNetworks | quote}} +- name: LINKERD2_PROXY_POLICY_SVC_ADDR + value: {{ternary "localhost.:8090" (printf "linkerd-policy.%s.svc.%s.:8090" .Release.Namespace .Values.clusterDomain) (eq (toString .Values.proxy.component) "linkerd-destination")}} +- name: LINKERD2_PROXY_POLICY_WORKLOAD + value: "$(_pod_ns):$(_pod_name)" +- name: LINKERD2_PROXY_INBOUND_DEFAULT_POLICY + value: {{.Values.proxy.defaultInboundPolicy}} +- name: LINKERD2_PROXY_POLICY_CLUSTER_NETWORKS + value: {{.Values.clusterNetworks | quote}} +{{ if .Values.proxy.inboundConnectTimeout -}} +- name: LINKERD2_PROXY_INBOUND_CONNECT_TIMEOUT + value: {{.Values.proxy.inboundConnectTimeout | quote}} +{{ end -}} +{{ if .Values.proxy.outboundConnectTimeout -}} +- name: LINKERD2_PROXY_OUTBOUND_CONNECT_TIMEOUT + value: {{.Values.proxy.outboundConnectTimeout | quote}} +{{ end -}} +{{ if .Values.proxy.outboundDiscoveryCacheUnusedTimeout -}} +- name: LINKERD2_PROXY_OUTBOUND_DISCOVERY_IDLE_TIMEOUT + value: {{.Values.proxy.outboundDiscoveryCacheUnusedTimeout | quote}} +{{ end -}} +{{ if .Values.proxy.inboundDiscoveryCacheUnusedTimeout -}} +- name: LINKERD2_PROXY_INBOUND_DISCOVERY_IDLE_TIMEOUT + value: {{.Values.proxy.inboundDiscoveryCacheUnusedTimeout | quote}} +{{ end -}} +{{ if .Values.proxy.disableOutboundProtocolDetectTimeout -}} +- name: LINKERD2_PROXY_OUTBOUND_DETECT_TIMEOUT + value: "365d" +{{ end -}} +{{ if .Values.proxy.disableInboundProtocolDetectTimeout -}} +- name: LINKERD2_PROXY_INBOUND_DETECT_TIMEOUT + value: "365d" +{{ end -}} +- name: LINKERD2_PROXY_CONTROL_LISTEN_ADDR + value: 0.0.0.0:{{.Values.proxy.ports.control}} +- name: LINKERD2_PROXY_ADMIN_LISTEN_ADDR + value: 0.0.0.0:{{.Values.proxy.ports.admin}} +- name: LINKERD2_PROXY_OUTBOUND_LISTEN_ADDR + value: 127.0.0.1:{{.Values.proxy.ports.outbound}} +- name: LINKERD2_PROXY_INBOUND_LISTEN_ADDR + value: 0.0.0.0:{{.Values.proxy.ports.inbound}} +- name: LINKERD2_PROXY_INBOUND_IPS + valueFrom: + fieldRef: + fieldPath: status.podIPs +- name: LINKERD2_PROXY_INBOUND_PORTS + value: {{ .Values.proxy.podInboundPorts | quote }} +{{ if .Values.proxy.isGateway -}} +- name: LINKERD2_PROXY_INBOUND_GATEWAY_SUFFIXES + value: {{printf "svc.%s." .Values.clusterDomain}} +{{ end -}} +{{ if .Values.proxy.isIngress -}} +- name: LINKERD2_PROXY_INGRESS_MODE + value: "true" +{{ end -}} +- name: LINKERD2_PROXY_DESTINATION_PROFILE_SUFFIXES + {{- $internalDomain := printf "svc.%s." .Values.clusterDomain }} + value: {{ternary "." $internalDomain .Values.proxy.enableExternalProfiles}} +- name: LINKERD2_PROXY_INBOUND_ACCEPT_KEEPALIVE + value: 10000ms +- name: LINKERD2_PROXY_OUTBOUND_CONNECT_KEEPALIVE + value: 10000ms +{{ if .Values.proxy.opaquePorts -}} +- name: LINKERD2_PROXY_INBOUND_PORTS_DISABLE_PROTOCOL_DETECTION + value: {{.Values.proxy.opaquePorts | quote}} +{{ end -}} +- name: LINKERD2_PROXY_DESTINATION_CONTEXT + value: | + {"ns":"$(_pod_ns)", "nodeName":"$(_pod_nodeName)", "pod":"$(_pod_name)"} +- name: _pod_sa + valueFrom: + fieldRef: + fieldPath: spec.serviceAccountName +- name: _l5d_ns + value: {{.Release.Namespace}} +- name: _l5d_trustdomain + value: {{$trustDomain}} +- name: LINKERD2_PROXY_IDENTITY_DIR + value: /var/run/linkerd/identity/end-entity +- name: LINKERD2_PROXY_IDENTITY_TRUST_ANCHORS +{{- /* +Pods in the `linkerd` namespace are not injected by the proxy injector and instead obtain +the trust anchor bundle from the `linkerd-identity-trust-roots` configmap. This should not +be used in other contexts. +*/}} +{{- if .Values.proxy.loadTrustBundleFromConfigMap }} + valueFrom: + configMapKeyRef: + name: linkerd-identity-trust-roots + key: ca-bundle.crt +{{ else }} + value: | + {{- required "Please provide the identity trust anchors" .Values.identityTrustAnchorsPEM | trim | nindent 4 }} +{{ end -}} +- name: LINKERD2_PROXY_IDENTITY_TOKEN_FILE +{{- if .Values.identity.serviceAccountTokenProjection }} + value: /var/run/secrets/tokens/linkerd-identity-token +{{ else }} + value: /var/run/secrets/kubernetes.io/serviceaccount/token +{{ end -}} +- name: LINKERD2_PROXY_IDENTITY_SVC_ADDR + value: {{ternary "localhost.:8080" (printf "linkerd-identity-headless.%s.svc.%s.:8080" .Release.Namespace .Values.clusterDomain) (eq (toString .Values.proxy.component) "linkerd-identity")}} +- name: LINKERD2_PROXY_IDENTITY_LOCAL_NAME + value: $(_pod_sa).$(_pod_ns).serviceaccount.identity.{{.Release.Namespace}}.{{$trustDomain}} +- name: LINKERD2_PROXY_IDENTITY_SVC_NAME + value: linkerd-identity.{{.Release.Namespace}}.serviceaccount.identity.{{.Release.Namespace}}.{{$trustDomain}} +- name: LINKERD2_PROXY_DESTINATION_SVC_NAME + value: linkerd-destination.{{.Release.Namespace}}.serviceaccount.identity.{{.Release.Namespace}}.{{$trustDomain}} +- name: LINKERD2_PROXY_POLICY_SVC_NAME + value: linkerd-destination.{{.Release.Namespace}}.serviceaccount.identity.{{.Release.Namespace}}.{{$trustDomain}} +{{ if .Values.proxy.accessLog -}} +- name: LINKERD2_PROXY_ACCESS_LOG + value: {{.Values.proxy.accessLog | quote}} +{{ end -}} +{{ if .Values.proxy.shutdownGracePeriod -}} +- name: LINKERD2_PROXY_SHUTDOWN_GRACE_PERIOD + value: {{.Values.proxy.shutdownGracePeriod | quote}} +{{ end -}} +image: {{.Values.proxy.image.name}}:{{.Values.proxy.image.version | default .Values.linkerdVersion}} +imagePullPolicy: {{.Values.proxy.image.pullPolicy | default .Values.imagePullPolicy}} +livenessProbe: + httpGet: + path: /live + port: {{.Values.proxy.ports.admin}} + initialDelaySeconds: 10 +name: linkerd-proxy +ports: +- containerPort: {{.Values.proxy.ports.inbound}} + name: linkerd-proxy +- containerPort: {{.Values.proxy.ports.admin}} + name: linkerd-admin +readinessProbe: + httpGet: + path: /ready + port: {{.Values.proxy.ports.admin}} + initialDelaySeconds: 2 +{{- if .Values.proxy.resources }} +{{ include "partials.resources" .Values.proxy.resources }} +{{- end }} +securityContext: + allowPrivilegeEscalation: false + {{- if .Values.proxy.capabilities -}} + {{- include "partials.proxy.capabilities" . | nindent 2 -}} + {{- end }} + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: {{.Values.proxy.uid}} + seccompProfile: + type: RuntimeDefault +terminationMessagePolicy: FallbackToLogsOnError +{{- if or (.Values.proxy.await) (.Values.proxy.waitBeforeExitSeconds) }} +lifecycle: +{{- if .Values.proxy.await }} + postStart: + exec: + command: + - /usr/lib/linkerd/linkerd-await + - --timeout=2m + - --port={{.Values.proxy.ports.admin}} +{{- end }} +{{- if .Values.proxy.waitBeforeExitSeconds }} + preStop: + exec: + command: + - /bin/sleep + - {{.Values.proxy.waitBeforeExitSeconds | quote}} +{{- end }} +{{- end }} +volumeMounts: +- mountPath: /var/run/linkerd/identity/end-entity + name: linkerd-identity-end-entity +{{- if .Values.identity.serviceAccountTokenProjection }} +- mountPath: /var/run/secrets/tokens + name: linkerd-identity-token +{{- end }} +{{- if .Values.proxy.saMountPath }} +- mountPath: {{.Values.proxy.saMountPath.mountPath}} + name: {{.Values.proxy.saMountPath.name}} + readOnly: {{.Values.proxy.saMountPath.readOnly}} +{{- end -}} +{{- end }} diff --git a/linkerd/charts/linkerd-viz/charts/partials/templates/_pull-secrets.tpl b/linkerd/charts/linkerd-viz/charts/partials/templates/_pull-secrets.tpl new file mode 100644 index 0000000..0c9aa4f --- /dev/null +++ b/linkerd/charts/linkerd-viz/charts/partials/templates/_pull-secrets.tpl @@ -0,0 +1,6 @@ +{{- define "partials.image-pull-secrets"}} +{{- if . }} +imagePullSecrets: +{{ toYaml . | indent 2 }} +{{- end }} +{{- end -}} diff --git a/linkerd/charts/linkerd-viz/charts/partials/templates/_resources.tpl b/linkerd/charts/linkerd-viz/charts/partials/templates/_resources.tpl new file mode 100644 index 0000000..1fd6789 --- /dev/null +++ b/linkerd/charts/linkerd-viz/charts/partials/templates/_resources.tpl @@ -0,0 +1,28 @@ +{{- define "partials.resources" -}} +{{- $ephemeralStorage := index . "ephemeral-storage" -}} +resources: + {{- if or (.cpu).limit (.memory).limit ($ephemeralStorage).limit }} + limits: + {{- with (.cpu).limit }} + cpu: {{. | quote}} + {{- end }} + {{- with (.memory).limit }} + memory: {{. | quote}} + {{- end }} + {{- with ($ephemeralStorage).limit }} + ephemeral-storage: {{. | quote}} + {{- end }} + {{- end }} + {{- if or (.cpu).request (.memory).request ($ephemeralStorage).request }} + requests: + {{- with (.cpu).request }} + cpu: {{. | quote}} + {{- end }} + {{- with (.memory).request }} + memory: {{. | quote}} + {{- end }} + {{- with ($ephemeralStorage).request }} + ephemeral-storage: {{. | quote}} + {{- end }} + {{- end }} +{{- end }} diff --git a/linkerd/charts/linkerd-viz/charts/partials/templates/_tolerations.tpl b/linkerd/charts/linkerd-viz/charts/partials/templates/_tolerations.tpl new file mode 100644 index 0000000..c2292b1 --- /dev/null +++ b/linkerd/charts/linkerd-viz/charts/partials/templates/_tolerations.tpl @@ -0,0 +1,4 @@ +{{- define "linkerd.tolerations" -}} +tolerations: +{{ toYaml .Values.tolerations | trim | indent 2 }} +{{- end -}} diff --git a/linkerd/charts/linkerd-viz/charts/partials/templates/_trace.tpl b/linkerd/charts/linkerd-viz/charts/partials/templates/_trace.tpl new file mode 100644 index 0000000..dee0595 --- /dev/null +++ b/linkerd/charts/linkerd-viz/charts/partials/templates/_trace.tpl @@ -0,0 +1,5 @@ +{{ define "partials.linkerd.trace" -}} +{{ if .Values.controlPlaneTracing -}} +- -trace-collector=collector.{{.Values.controlPlaneTracingNamespace}}.svc.{{.Values.clusterDomain}}:55678 +{{ end -}} +{{- end }} diff --git a/linkerd/charts/linkerd-viz/charts/partials/templates/_validate.tpl b/linkerd/charts/linkerd-viz/charts/partials/templates/_validate.tpl new file mode 100644 index 0000000..ba772c2 --- /dev/null +++ b/linkerd/charts/linkerd-viz/charts/partials/templates/_validate.tpl @@ -0,0 +1,19 @@ +{{- define "linkerd.webhook.validation" -}} + +{{- if and (.injectCaFrom) (.injectCaFromSecret) -}} +{{- fail "injectCaFrom and injectCaFromSecret cannot both be set" -}} +{{- end -}} + +{{- if and (or (.injectCaFrom) (.injectCaFromSecret)) (.caBundle) -}} +{{- fail "injectCaFrom or injectCaFromSecret cannot be set if providing a caBundle" -}} +{{- end -}} + +{{- if and (.externalSecret) (empty .caBundle) (empty .injectCaFrom) (empty .injectCaFromSecret) -}} +{{- fail "if externalSecret is set, then caBundle, injectCaFrom, or injectCaFromSecret must be set" -}} +{{- end }} + +{{- if and (or .injectCaFrom .injectCaFromSecret .caBundle) (not .externalSecret) -}} +{{- fail "if caBundle, injectCaFrom, or injectCaFromSecret is set, then externalSecret must be set" -}} +{{- end -}} + +{{- end -}} diff --git a/linkerd/charts/linkerd-viz/charts/partials/templates/_volumes.tpl b/linkerd/charts/linkerd-viz/charts/partials/templates/_volumes.tpl new file mode 100644 index 0000000..9684cf2 --- /dev/null +++ b/linkerd/charts/linkerd-viz/charts/partials/templates/_volumes.tpl @@ -0,0 +1,20 @@ +{{ define "partials.proxy.volumes.identity" -}} +emptyDir: + medium: Memory +name: linkerd-identity-end-entity +{{- end -}} + +{{ define "partials.proxyInit.volumes.xtables" -}} +emptyDir: {} +name: {{ .Values.proxyInit.xtMountPath.name }} +{{- end -}} + +{{- define "partials.proxy.volumes.service-account-token" -}} +name: linkerd-identity-token +projected: + sources: + - serviceAccountToken: + path: linkerd-identity-token + expirationSeconds: 86400 {{- /* # 24 hours */}} + audience: identity.l5d.io +{{- end -}} diff --git a/linkerd/charts/linkerd-viz/charts/partials/values.yaml b/linkerd/charts/linkerd-viz/charts/partials/values.yaml new file mode 100644 index 0000000..e69de29 diff --git a/linkerd/charts/linkerd-viz/requirements.lock b/linkerd/charts/linkerd-viz/requirements.lock new file mode 100644 index 0000000..3187cab --- /dev/null +++ b/linkerd/charts/linkerd-viz/requirements.lock @@ -0,0 +1,6 @@ +dependencies: +- name: partials + repository: file://../../../charts/partials + version: 0.1.0 +digest: sha256:e2c1d0d581afb33df46411df7a89fca2628328fc7bd0975167e7812bf128e27f +generated: "2020-12-07T13:36:20.311112349Z" diff --git a/linkerd/charts/linkerd-viz/requirements.yaml b/linkerd/charts/linkerd-viz/requirements.yaml new file mode 100644 index 0000000..585d71b --- /dev/null +++ b/linkerd/charts/linkerd-viz/requirements.yaml @@ -0,0 +1,4 @@ +dependencies: +- name: partials + version: 0.1.0 + repository: file://../../../charts/partials diff --git a/linkerd/charts/linkerd-viz/templates/NOTES.txt b/linkerd/charts/linkerd-viz/templates/NOTES.txt new file mode 100644 index 0000000..8c88af1 --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/NOTES.txt @@ -0,0 +1,11 @@ +The Linkerd Viz extension was successfully installed 🎉 + +To make sure everything works as expected, run the following: + + linkerd viz check + +To view the linkerd dashboard, run the following: + + linkerd viz dashboard + +Looking for more? Visit https://linkerd.io/2/getting-started/ diff --git a/linkerd/charts/linkerd-viz/templates/admin-policy.yaml b/linkerd/charts/linkerd-viz/templates/admin-policy.yaml new file mode 100644 index 0000000..eed8a11 --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/admin-policy.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: policy.linkerd.io/v1alpha1 +kind: NetworkAuthentication +metadata: + namespace: {{ .Release.Namespace }} + name: kubelet + labels: + linkerd.io/extension: viz + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + # Ideally, this should be restricted to the actual set of IPs kubelet uses in + # a cluster. This can't easily be discovered. + networks: + - cidr: "0.0.0.0/0" + - cidr: "::/0" diff --git a/linkerd/charts/linkerd-viz/templates/metrics-api-policy.yaml b/linkerd/charts/linkerd-viz/templates/metrics-api-policy.yaml new file mode 100644 index 0000000..83f631a --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/metrics-api-policy.yaml @@ -0,0 +1,56 @@ +--- +apiVersion: policy.linkerd.io/v1beta1 +kind: Server +metadata: + namespace: {{ .Release.Namespace }} + name: metrics-api + labels: + linkerd.io/extension: viz + component: metrics-api + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + podSelector: + matchLabels: + linkerd.io/extension: viz + component: metrics-api + port: http + proxyProtocol: HTTP/1 +--- +apiVersion: policy.linkerd.io/v1alpha1 +kind: AuthorizationPolicy +metadata: + namespace: {{ .Release.Namespace }} + name: metrics-api + labels: + linkerd.io/extension: viz + component: metrics-api + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + targetRef: + group: policy.linkerd.io + kind: Server + name: metrics-api + requiredAuthenticationRefs: + - group: policy.linkerd.io + kind: MeshTLSAuthentication + name: metrics-api-web +--- +apiVersion: policy.linkerd.io/v1alpha1 +kind: MeshTLSAuthentication +metadata: + namespace: {{ .Release.Namespace }} + name: metrics-api-web + labels: + linkerd.io/extension: viz + component: metrics-api + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + identityRefs: + - kind: ServiceAccount + name: web diff --git a/linkerd/charts/linkerd-viz/templates/metrics-api-rbac.yaml b/linkerd/charts/linkerd-viz/templates/metrics-api-rbac.yaml new file mode 100644 index 0000000..2fa38a8 --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/metrics-api-rbac.yaml @@ -0,0 +1,56 @@ +--- +### +### Metrics API RBAC +### +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: linkerd-{{.Release.Namespace}}-metrics-api + labels: + linkerd.io/extension: viz + component: metrics-api + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +rules: +- apiGroups: ["extensions", "apps"] + resources: ["daemonsets", "deployments", "replicasets", "statefulsets"] + verbs: ["list", "get", "watch"] +- apiGroups: ["extensions", "batch"] + resources: ["cronjobs", "jobs"] + verbs: ["list" , "get", "watch"] +- apiGroups: [""] + resources: ["pods", "endpoints", "services", "replicationcontrollers", "namespaces"] + verbs: ["list", "get", "watch"] +- apiGroups: ["linkerd.io"] + resources: ["serviceprofiles"] + verbs: ["list", "get", "watch"] +- apiGroups: ["policy.linkerd.io"] + resources: ["servers", "serverauthorizations", "authorizationpolicies", "httproutes"] + verbs: ["list", "get"] +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: linkerd-{{.Release.Namespace}}-metrics-api + labels: + linkerd.io/extension: viz + component: metrics-api + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: linkerd-{{.Release.Namespace}}-metrics-api +subjects: +- kind: ServiceAccount + name: metrics-api + namespace: {{.Release.Namespace}} +--- +kind: ServiceAccount +apiVersion: v1 +metadata: + name: metrics-api + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/extension: viz + component: metrics-api + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +{{- include "partials.image-pull-secrets" .Values.imagePullSecrets }} diff --git a/linkerd/charts/linkerd-viz/templates/metrics-api.yaml b/linkerd/charts/linkerd-viz/templates/metrics-api.yaml new file mode 100644 index 0000000..1a84f7f --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/metrics-api.yaml @@ -0,0 +1,125 @@ +--- +### +### Metrics API +### +kind: Service +apiVersion: v1 +metadata: + name: metrics-api + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/extension: viz + component: metrics-api + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} + {{- with .Values.metricsAPI.service.annotations }}{{ toYaml . | trim | nindent 4 }}{{- end }} + linkerd.io/inject: enabled +spec: + type: ClusterIP + selector: + linkerd.io/extension: viz + component: metrics-api + ports: + - name: http + port: 8085 + targetPort: 8085 +--- +{{- $tree := deepCopy . }} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + {{ include "partials.annotations.created-by" . }} + linkerd.io/inject: enabled + config.linkerd.io/proxy-await: "enabled" + labels: + linkerd.io/extension: viz + app.kubernetes.io/name: metrics-api + app.kubernetes.io/part-of: Linkerd + app.kubernetes.io/version: {{default .Values.linkerdVersion .Values.cliVersion}} + component: metrics-api + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + name: metrics-api + namespace: {{ .Release.Namespace }} +spec: + replicas: {{.Values.metricsAPI.replicas}} + selector: + matchLabels: + linkerd.io/extension: viz + component: metrics-api + template: + metadata: + annotations: + {{- if empty .Values.cliVersion }} + checksum/config: {{ include (print $.Template.BasePath "/metrics-api-rbac.yaml") . | sha256sum }} + {{- end }} + {{ include "partials.annotations.created-by" . }} + {{- with .Values.metricsAPI.proxy }} + {{- include "partials.proxy.config.annotations" .resources | nindent 8 }} + {{- end }} + {{- with .Values.podAnnotations }}{{ toYaml . | trim | nindent 8 }}{{- end }} + linkerd.io/inject: enabled + config.alpha.linkerd.io/proxy-wait-before-exit-seconds: "0" + cluster-autoscaler.kubernetes.io/safe-to-evict: "true" + labels: + linkerd.io/extension: viz + component: metrics-api + {{- with .Values.podLabels }}{{ toYaml . | trim | nindent 8 }}{{- end }} + spec: + {{- if .Values.metricsAPI.tolerations -}} + {{- include "linkerd.tolerations" (dict "Values" .Values.metricsAPI) | nindent 6 }} + {{- end -}} + {{- include "linkerd.node-selector" (dict "Values" .Values.metricsAPI) | nindent 6 }} + {{- $_ := set $tree "component" "metrics-api" -}} + {{- $_ := set $tree "label" "component" -}} + {{- include "linkerd.affinity" $tree | nindent 6 }} + containers: + - args: + - -controller-namespace={{.Values.linkerdNamespace}} + - -log-level={{.Values.metricsAPI.logLevel | default .Values.defaultLogLevel}} + - -log-format={{.Values.metricsAPI.logFormat | default .Values.defaultLogFormat}} + - -cluster-domain={{.Values.clusterDomain}} + {{- if .Values.prometheusUrl }} + - -prometheus-url={{.Values.prometheusUrl}} + {{- else if .Values.prometheus.enabled }} + - -prometheus-url=http://prometheus.{{.Release.Namespace}}.svc.{{.Values.clusterDomain}}:9090 + {{- else }} + {{ fail "Please enable `linkerd-prometheus` or provide `prometheusUrl` for the viz extension to function properly"}} + {{- end }} + - -enable-pprof={{.Values.enablePprof | default false}} + image: {{.Values.metricsAPI.image.registry | default .Values.defaultRegistry}}/{{.Values.metricsAPI.image.name}}:{{.Values.metricsAPI.image.tag | default .Values.linkerdVersion}} + imagePullPolicy: {{.Values.metricsAPI.image.pullPolicy | default .Values.defaultImagePullPolicy}} + livenessProbe: + httpGet: + path: /ping + port: 9995 + initialDelaySeconds: 10 + name: metrics-api + ports: + - containerPort: 8085 + name: http + - containerPort: 9995 + name: admin-http + readinessProbe: + failureThreshold: 7 + httpGet: + path: /ready + port: 9995 + {{- if .Values.metricsAPI.resources -}} + {{- include "partials.resources" .Values.metricsAPI.resources | nindent 8 }} + {{- end }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + readOnlyRootFilesystem: true + runAsUser: {{.Values.metricsAPI.UID | default .Values.defaultUID}} + seccompProfile: + type: RuntimeDefault + securityContext: + seccompProfile: + type: RuntimeDefault + serviceAccountName: metrics-api diff --git a/linkerd/charts/linkerd-viz/templates/namespace-metadata-rbac.yaml b/linkerd/charts/linkerd-viz/templates/namespace-metadata-rbac.yaml new file mode 100644 index 0000000..433f350 --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/namespace-metadata-rbac.yaml @@ -0,0 +1,77 @@ +kind: ServiceAccount +apiVersion: v1 +metadata: + labels: + linkerd.io/extension: viz + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} + "helm.sh/hook": post-install + "helm.sh/hook-weight": "0" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + name: namespace-metadata + namespace: {{.Release.Namespace}} +{{- include "partials.image-pull-secrets" .Values.imagePullSecrets }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + linkerd.io/extension: viz + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} + "helm.sh/hook": post-install + "helm.sh/hook-weight": "0" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + name: namespace-metadata + namespace: {{.Release.Namespace}} +rules: +- apiGroups: [""] + resources: ["namespaces"] + verbs: ["get", "patch"] + resourceNames: ["{{.Release.Namespace}}"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + linkerd.io/extension: viz + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} + "helm.sh/hook": post-install + "helm.sh/hook-weight": "0" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + name: namespace-metadata + namespace: {{.Release.Namespace}} +roleRef: + kind: Role + name: namespace-metadata + apiGroup: rbac.authorization.k8s.io +subjects: +- kind: ServiceAccount + name: namespace-metadata + namespace: {{.Release.Namespace}} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + namespace: {{ .Values.linkerdNamespace }} + labels: + linkerd.io/extension: viz + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} + "helm.sh/hook": post-install + "helm.sh/hook-weight": "0" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + name: viz-namespace-metadata-linkerd-config +roleRef: + kind: Role + name: ext-namespace-metadata-linkerd-config + apiGroup: rbac.authorization.k8s.io +subjects: +- kind: ServiceAccount + name: namespace-metadata + namespace: {{.Release.Namespace}} diff --git a/linkerd/charts/linkerd-viz/templates/namespace-metadata.yaml b/linkerd/charts/linkerd-viz/templates/namespace-metadata.yaml new file mode 100644 index 0000000..b6b577a --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/namespace-metadata.yaml @@ -0,0 +1,67 @@ +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + {{ include "partials.annotations.created-by" . }} + "helm.sh/hook": post-install + "helm.sh/hook-weight": "1" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + labels: + linkerd.io/extension: viz + app.kubernetes.io/name: namespace-metadata + app.kubernetes.io/part-of: Linkerd + app.kubernetes.io/version: {{default .Values.linkerdVersion .Values.cliVersion}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + name: namespace-metadata + namespace: {{.Release.Namespace}} +spec: + template: + metadata: + annotations: + {{ include "partials.annotations.created-by" . }} + linkerd.io/inject: disabled + labels: + linkerd.io/extension: viz + app.kubernetes.io/name: namespace-metadata + app.kubernetes.io/part-of: Linkerd + app.kubernetes.io/version: {{default .Values.linkerdVersion .Values.cliVersion}} + {{- with .Values.podLabels }}{{ toYaml . | trim | nindent 8 }}{{- end }} + spec: + {{- if .Values.namespaceMetadata.tolerations -}} + {{- include "linkerd.tolerations" (dict "Values" .Values.namespaceMetadata) | nindent 6 }} + {{- end -}} + {{- include "linkerd.node-selector" (dict "Values" .Values.namespaceMetadata) | nindent 6 }} + restartPolicy: Never + securityContext: + seccompProfile: + type: RuntimeDefault + serviceAccountName: namespace-metadata + containers: + - name: namespace-metadata + image: {{.Values.namespaceMetadata.image.registry | default .Values.defaultRegistry}}/{{.Values.namespaceMetadata.image.name}}:{{.Values.namespaceMetadata.image.tag}} + imagePullPolicy: {{.Values.namespaceMetadata.image.pullPolicy | default .Values.defaultImagePullPolicy}} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + readOnlyRootFilesystem: true + runAsUser: {{.Values.defaultUID}} + seccompProfile: + type: RuntimeDefault + args: + - --log-format + - {{.Values.defaultLogFormat}} + - --log-level + - {{.Values.defaultLogLevel}} + - --extension + - viz + - --namespace + - {{.Release.Namespace}} + - --linkerd-namespace + - {{.Values.linkerdNamespace}} + {{- with .Values.prometheusUrl }} + - --prometheus-url + - {{.}} + {{- end }} diff --git a/linkerd/charts/linkerd-viz/templates/namespace.yaml b/linkerd/charts/linkerd-viz/templates/namespace.yaml new file mode 100644 index 0000000..4cb1d2c --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/namespace.yaml @@ -0,0 +1,18 @@ +{{- if eq .Release.Service "CLI" -}} +--- +### +### Linkerd Viz Extension Namespace +### +kind: Namespace +apiVersion: v1 +metadata: + name: {{.Release.Namespace}} + labels: + linkerd.io/extension: viz + {{- /* linkerd-init requires extended capabilities and so requires priviledged mode */}} + pod-security.kubernetes.io/enforce: {{ if .Values.cniEnabled }}restricted{{ else }}privileged{{ end }} + annotations: + {{- if .Values.prometheusUrl }} + viz.linkerd.io/external-prometheus: {{.Values.prometheusUrl}} + {{- end }} +{{ end -}} diff --git a/linkerd/charts/linkerd-viz/templates/prometheus-policy.yaml b/linkerd/charts/linkerd-viz/templates/prometheus-policy.yaml new file mode 100644 index 0000000..1db3a1b --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/prometheus-policy.yaml @@ -0,0 +1,39 @@ +--- +apiVersion: policy.linkerd.io/v1beta1 +kind: Server +metadata: + namespace: {{ .Release.Namespace }} + name: prometheus-admin + labels: + linkerd.io/extension: viz + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + podSelector: + matchLabels: + linkerd.io/extension: viz + component: prometheus + namespace: {{.Release.Namespace}} + port: admin-http + proxyProtocol: HTTP/1 +--- +apiVersion: policy.linkerd.io/v1alpha1 +kind: AuthorizationPolicy +metadata: + namespace: {{ .Release.Namespace }} + name: prometheus-admin + labels: + linkerd.io/extension: viz + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + targetRef: + group: policy.linkerd.io + kind: Server + name: prometheus-admin + requiredAuthenticationRefs: + - kind: ServiceAccount + name: metrics-api + namespace: {{.Release.Namespace}} diff --git a/linkerd/charts/linkerd-viz/templates/prometheus-rbac.yaml b/linkerd/charts/linkerd-viz/templates/prometheus-rbac.yaml new file mode 100644 index 0000000..bbde420 --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/prometheus-rbac.yaml @@ -0,0 +1,47 @@ +{{ if .Values.prometheus.enabled -}} +--- +### +### Prometheus RBAC +### +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: linkerd-{{.Release.Namespace}}-prometheus + labels: + linkerd.io/extension: viz + component: prometheus + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +rules: +- apiGroups: [""] + resources: ["nodes", "nodes/proxy", "pods"] + verbs: ["get", "list", "watch"] +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: linkerd-{{.Release.Namespace}}-prometheus + labels: + linkerd.io/extension: viz + component: prometheus + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: linkerd-{{.Release.Namespace}}-prometheus +subjects: +- kind: ServiceAccount + name: prometheus + namespace: {{.Release.Namespace}} +--- +kind: ServiceAccount +apiVersion: v1 +metadata: + name: prometheus + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/extension: viz + component: prometheus + namespace: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +{{- include "partials.image-pull-secrets" .Values.imagePullSecrets }} +{{ end -}} diff --git a/linkerd/charts/linkerd-viz/templates/prometheus.yaml b/linkerd/charts/linkerd-viz/templates/prometheus.yaml new file mode 100644 index 0000000..d7615c0 --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/prometheus.yaml @@ -0,0 +1,337 @@ +{{ if .Values.prometheus.enabled -}} +--- +### +### Prometheus +### +kind: ConfigMap +apiVersion: v1 +metadata: + name: prometheus-config + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/extension: viz + component: prometheus + namespace: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +data: + prometheus.yml: |- + global: + {{- if .Values.prometheus.globalConfig -}} + {{- toYaml .Values.prometheus.globalConfig | trim | nindent 6 }} + {{- end}} + + rule_files: + - /etc/prometheus/*_rules.yml + - /etc/prometheus/*_rules.yaml + + scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + + # Required for: https://grafana.com/grafana/dashboards/315 + - job_name: 'kubernetes-nodes-cadvisor' + scheme: https + tls_config: + ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt + insecure_skip_verify: true + bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token + kubernetes_sd_configs: + - role: node + relabel_configs: + - action: labelmap + regex: __meta_kubernetes_node_label_(.+) + - target_label: __address__ + replacement: kubernetes.default.svc:443 + - source_labels: [__meta_kubernetes_node_name] + regex: (.+) + target_label: __metrics_path__ + replacement: /api/v1/nodes/$1/proxy/metrics/cadvisor + metric_relabel_configs: + - source_labels: [__name__] + regex: '(container|machine)_(cpu|memory|network|fs)_(.+)' + action: keep + - source_labels: [__name__] + regex: 'container_memory_failures_total' # unneeded large metric + action: drop + + - job_name: 'linkerd-controller' + kubernetes_sd_configs: + - role: pod + namespaces: + names: + - '{{.Values.linkerdNamespace}}' + - '{{.Release.Namespace}}' + relabel_configs: + - source_labels: + - __meta_kubernetes_pod_container_port_name + action: keep + regex: admin-http + - source_labels: [__meta_kubernetes_pod_container_name] + action: replace + target_label: component + + - job_name: 'linkerd-service-mirror' + kubernetes_sd_configs: + - role: pod + relabel_configs: + - source_labels: + - __meta_kubernetes_pod_label_component + - __meta_kubernetes_pod_container_port_name + action: keep + regex: linkerd-service-mirror;admin-http$ + - source_labels: [__meta_kubernetes_pod_container_name] + action: replace + target_label: component + + - job_name: 'linkerd-proxy' + kubernetes_sd_configs: + - role: pod + relabel_configs: + - source_labels: + - __meta_kubernetes_pod_container_name + - __meta_kubernetes_pod_container_port_name + - __meta_kubernetes_pod_label_linkerd_io_control_plane_ns + action: keep + regex: ^{{default .Values.proxyContainerName "linkerd-proxy" .Values.proxyContainerName}};linkerd-admin;{{.Values.linkerdNamespace}}$ + - source_labels: [__meta_kubernetes_namespace] + action: replace + target_label: namespace + - source_labels: [__meta_kubernetes_pod_name] + action: replace + target_label: pod + # special case k8s' "job" label, to not interfere with prometheus' "job" + # label + # __meta_kubernetes_pod_label_linkerd_io_proxy_job=foo => + # k8s_job=foo + - source_labels: [__meta_kubernetes_pod_label_linkerd_io_proxy_job] + action: replace + target_label: k8s_job + # drop __meta_kubernetes_pod_label_linkerd_io_proxy_job + - action: labeldrop + regex: __meta_kubernetes_pod_label_linkerd_io_proxy_job + # __meta_kubernetes_pod_label_linkerd_io_proxy_deployment=foo => + # deployment=foo + - action: labelmap + regex: __meta_kubernetes_pod_label_linkerd_io_proxy_(.+) + # drop all labels that we just made copies of in the previous labelmap + - action: labeldrop + regex: __meta_kubernetes_pod_label_linkerd_io_proxy_(.+) + # __meta_kubernetes_pod_label_linkerd_io_foo=bar => + # foo=bar + - action: labelmap + regex: __meta_kubernetes_pod_label_linkerd_io_(.+) + # Copy all pod labels to tmp labels + - action: labelmap + regex: __meta_kubernetes_pod_label_(.+) + replacement: __tmp_pod_label_$1 + # Take `linkerd_io_` prefixed labels and copy them without the prefix + - action: labelmap + regex: __tmp_pod_label_linkerd_io_(.+) + replacement: __tmp_pod_label_$1 + # Drop the `linkerd_io_` originals + - action: labeldrop + regex: __tmp_pod_label_linkerd_io_(.+) + # Copy tmp labels into real labels + - action: labelmap + regex: __tmp_pod_label_(.+) + + {{- if .Values.prometheus.scrapeConfigs }} + {{- toYaml .Values.prometheus.scrapeConfigs | trim | nindent 4 }} + {{- end }} + + {{- if (or .Values.prometheus.alertmanagers .Values.prometheus.alertRelabelConfigs) }} + alerting: + alert_relabel_configs: + {{- if .Values.prometheus.alertRelabelConfigs }} + {{- toYaml .Values.prometheus.alertRelabelConfigs | trim | nindent 6 }} + {{- end }} + alertmanagers: + {{- if .Values.prometheus.alertmanagers }} + {{- toYaml .Values.prometheus.alertmanagers | trim | nindent 6 }} + {{- end }} + {{- end }} + + {{- if .Values.prometheus.remoteWrite }} + remote_write: + {{- toYaml .Values.prometheus.remoteWrite | trim | nindent 6 }} + {{- end }} +--- +kind: Service +apiVersion: v1 +metadata: + name: prometheus + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/extension: viz + component: prometheus + namespace: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} + linkerd.io/inject: enabled +spec: + type: ClusterIP + selector: + linkerd.io/extension: viz + component: prometheus + ports: + - name: admin-http + port: 9090 + targetPort: 9090 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + {{ include "partials.annotations.created-by" . }} + linkerd.io/inject: enabled + config.linkerd.io/proxy-await: "enabled" + labels: + linkerd.io/extension: viz + app.kubernetes.io/name: prometheus + app.kubernetes.io/part-of: Linkerd + app.kubernetes.io/version: {{default .Values.linkerdVersion .Values.cliVersion}} + component: prometheus + namespace: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + name: prometheus + namespace: {{ .Release.Namespace }} +spec: + replicas: 1 + {{- if .Values.prometheus.persistence }} + strategy: + type: Recreate + {{- end }} + selector: + matchLabels: + linkerd.io/extension: viz + component: prometheus + namespace: {{.Release.Namespace}} + template: + metadata: + annotations: + {{ include "partials.annotations.created-by" . }} + {{- with .Values.prometheus.proxy }} + {{- include "partials.proxy.config.annotations" .resources | nindent 8 }} + {{- end }} + {{- with .Values.podAnnotations }}{{ toYaml . | trim | nindent 8 }}{{- end }} + linkerd.io/inject: enabled + config.alpha.linkerd.io/proxy-wait-before-exit-seconds: "0" + labels: + linkerd.io/extension: viz + component: prometheus + namespace: {{.Release.Namespace}} + {{- with .Values.podLabels }}{{ toYaml . | trim | nindent 8 }}{{- end }} + spec: + {{- if .Values.prometheus.tolerations -}} + {{- include "linkerd.tolerations" (dict "Values" .Values.prometheus) | nindent 6 }} + {{- end -}} + {{- include "linkerd.node-selector" (dict "Values" .Values.prometheus) | nindent 6 }} + containers: + {{- if .Values.prometheus.sidecarContainers -}} + {{- toYaml .Values.prometheus.sidecarContainers | trim | nindent 6 }} + {{- end}} + - args: + {{- if not (hasKey .Values.prometheus.args "log.level") }} + - --log.level={{.Values.prometheus.logLevel | default .Values.defaultLogLevel}} + {{- end }} + {{- if not (hasKey .Values.prometheus.args "log.format") }} + - --log.format={{.Values.prometheus.logFormat | default .Values.defaultLogFormat | replace "plain" "logfmt" }} + {{- end }} + {{- range $key, $value := .Values.prometheus.args}} + - --{{ $key }}{{ if $value }}={{ $value }}{{ end }} + {{- end }} + image: {{.Values.prometheus.image.registry}}/{{.Values.prometheus.image.name}}:{{.Values.prometheus.image.tag}} + imagePullPolicy: {{.Values.prometheus.image.pullPolicy | default .Values.defaultImagePullPolicy}} + livenessProbe: + httpGet: + path: /-/healthy + port: 9090 + initialDelaySeconds: 30 + timeoutSeconds: 30 + name: prometheus + ports: + - containerPort: 9090 + name: admin-http + readinessProbe: + httpGet: + path: /-/ready + port: 9090 + initialDelaySeconds: 30 + timeoutSeconds: 30 + {{- if .Values.prometheus.resources -}} + {{- include "partials.resources" .Values.prometheus.resources | nindent 8 }} + {{- end }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 65534 + runAsGroup: 65534 + seccompProfile: + type: RuntimeDefault + volumeMounts: + {{- range .Values.prometheus.ruleConfigMapMounts }} + - name: {{ .name }} + mountPath: /etc/prometheus/{{ .subPath }} + subPath: {{ .subPath }} + readOnly: true + {{- end }} + - mountPath: /data + name: data + - mountPath: /etc/prometheus/prometheus.yml + name: prometheus-config + subPath: prometheus.yml + readOnly: true + securityContext: + fsGroup: 65534 + seccompProfile: + type: RuntimeDefault + serviceAccountName: prometheus + volumes: + {{- range .Values.prometheus.ruleConfigMapMounts }} + - name: {{ .name }} + configMap: + name: {{ .configMap }} + {{- end }} + - name: data + {{- if .Values.prometheus.persistence }} + persistentVolumeClaim: + claimName: prometheus + {{- else }} + emptyDir: {} + {{- end }} + - configMap: + name: prometheus-config + name: prometheus-config +{{- if .Values.prometheus.persistence }} +--- +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + labels: + linkerd.io/extension: viz + app.kubernetes.io/name: prometheus + app.kubernetes.io/part-of: Linkerd + app.kubernetes.io/version: {{default .Values.linkerdVersion}} + component: prometheus + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + name: prometheus + namespace: {{ .Release.Namespace }} +spec: + accessModes: + - {{ .Values.prometheus.persistence.accessMode | quote }} + resources: + requests: + storage: {{ .Values.prometheus.persistence.size | quote }} +{{- if .Values.prometheus.persistence.storageClass }} + storageClassName: "{{ .Values.prometheus.persistence.storageClass }}" +{{- end }} +{{- end }} +{{ end -}} diff --git a/linkerd/charts/linkerd-viz/templates/psp.yaml b/linkerd/charts/linkerd-viz/templates/psp.yaml new file mode 100644 index 0000000..6392754 --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/psp.yaml @@ -0,0 +1,52 @@ +{{ if .Values.enablePSP -}} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: psp + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/extension: viz + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +rules: +- apiGroups: ['policy', 'extensions'] + resources: ['podsecuritypolicies'] + verbs: ['use'] + resourceNames: + - linkerd-{{.Values.linkerdNamespace}}-control-plane +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: viz-psp + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/extension: viz + namespace: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +roleRef: + kind: Role + name: psp + apiGroup: rbac.authorization.k8s.io +subjects: +- kind: ServiceAccount + name: tap + namespace: {{.Release.Namespace}} +- kind: ServiceAccount + name: web + namespace: {{.Release.Namespace}} +{{ if .Values.prometheus.enabled -}} +- kind: ServiceAccount + name: prometheus + namespace: {{.Release.Namespace}} +{{ end -}} +- kind: ServiceAccount + name: metrics-api + namespace: {{.Release.Namespace}} +- kind: ServiceAccount + name: tap-injector + namespace: {{.Release.Namespace}} +- kind: ServiceAccount + name: namespace-metadata + namespace: {{.Release.Namespace}} +{{ end -}} diff --git a/linkerd/charts/linkerd-viz/templates/service-profiles.yaml b/linkerd/charts/linkerd-viz/templates/service-profiles.yaml new file mode 100644 index 0000000..fd3512a --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/service-profiles.yaml @@ -0,0 +1,64 @@ +--- +apiVersion: linkerd.io/v1alpha2 +kind: ServiceProfile +metadata: + name: metrics-api.{{.Release.Namespace}}.svc.{{.Values.clusterDomain}} + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/extension: viz + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +spec: + routes: + - name: POST /api/v1/StatSummary + condition: + method: POST + pathRegex: /api/v1/StatSummary + - name: POST /api/v1/TopRoutes + condition: + method: POST + pathRegex: /api/v1/TopRoutes + - name: POST /api/v1/ListPods + condition: + method: POST + pathRegex: /api/v1/ListPods + - name: POST /api/v1/ListServices + condition: + method: POST + pathRegex: /api/v1/ListServices + - name: POST /api/v1/SelfCheck + condition: + method: POST + pathRegex: /api/v1/SelfCheck + - name: POST /api/v1/Gateways + condition: + method: POST + pathRegex: /api/v1/Gateways + - name: POST /api/v1/Edges + condition: + method: POST + pathRegex: /api/v1/Edges +{{ if .Values.prometheus.enabled -}} +--- +apiVersion: linkerd.io/v1alpha2 +kind: ServiceProfile +metadata: + name: prometheus.{{.Release.Namespace}}.svc.{{.Values.clusterDomain}} + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/extension: viz + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +spec: + routes: + - name: POST /api/v1/query + condition: + method: POST + pathRegex: /api/v1/query + - name: GET /api/v1/query_range + condition: + method: GET + pathRegex: /api/v1/query_range + - name: GET /api/v1/series + condition: + method: GET + pathRegex: /api/v1/series +{{ end -}} diff --git a/linkerd/charts/linkerd-viz/templates/tap-injector-policy.yaml b/linkerd/charts/linkerd-viz/templates/tap-injector-policy.yaml new file mode 100644 index 0000000..78692a5 --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/tap-injector-policy.yaml @@ -0,0 +1,57 @@ +--- +apiVersion: policy.linkerd.io/v1beta1 +kind: Server +metadata: + namespace: {{ .Release.Namespace }} + name: tap-injector-webhook + labels: + linkerd.io/extension: viz + component: tap-injector + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + podSelector: + matchLabels: + linkerd.io/extension: viz + component: tap-injector + port: tap-injector + proxyProtocol: TLS +--- +apiVersion: policy.linkerd.io/v1alpha1 +kind: AuthorizationPolicy +metadata: + namespace: {{ .Release.Namespace }} + name: tap-injector + labels: + linkerd.io/extension: viz + component: tap-injector + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + targetRef: + group: policy.linkerd.io + kind: Server + name: tap-injector-webhook + requiredAuthenticationRefs: + - group: policy.linkerd.io + kind: NetworkAuthentication + name: kube-api-server +--- +apiVersion: policy.linkerd.io/v1alpha1 +kind: NetworkAuthentication +metadata: + namespace: {{ .Release.Namespace }} + name: kube-api-server + labels: + linkerd.io/extension: viz + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + # Ideally, this should be restricted to the actual set of IPs the kubelet API + # server uses for webhooks in a cluster. This can't easily be discovered. + networks: + - cidr: "0.0.0.0/0" + - cidr: "::/0" diff --git a/linkerd/charts/linkerd-viz/templates/tap-injector-rbac.yaml b/linkerd/charts/linkerd-viz/templates/tap-injector-rbac.yaml new file mode 100644 index 0000000..f4e915e --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/tap-injector-rbac.yaml @@ -0,0 +1,105 @@ +--- +### +### Tap Injector RBAC +### +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: linkerd-tap-injector + labels: + linkerd.io/extension: viz + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +rules: +- apiGroups: [""] + resources: ["namespaces"] + verbs: ["get", "list", "watch"] +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: linkerd-tap-injector + labels: + linkerd.io/extension: viz + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +subjects: +- kind: ServiceAccount + name: tap-injector + namespace: {{.Release.Namespace}} +roleRef: + kind: ClusterRole + name: linkerd-tap-injector + apiGroup: rbac.authorization.k8s.io +--- +kind: ServiceAccount +apiVersion: v1 +metadata: + name: tap-injector + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/extension: viz + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +{{- include "partials.image-pull-secrets" .Values.imagePullSecrets }} +--- +{{- $host := printf "tap-injector.%s.svc" .Release.Namespace }} +{{- $ca := genSelfSignedCert $host (list) (list $host) 365 }} +{{- if (not .Values.tapInjector.externalSecret) }} +kind: Secret +apiVersion: v1 +metadata: + name: tap-injector-k8s-tls + namespace: {{ .Release.Namespace }} + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + linkerd.io/extension: viz + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +type: kubernetes.io/tls +data: + tls.crt: {{ ternary (b64enc (trim $ca.Cert)) (b64enc (trim .Values.tapInjector.crtPEM)) (empty .Values.tapInjector.crtPEM) }} + tls.key: {{ ternary (b64enc (trim $ca.Key)) (b64enc (trim .Values.tapInjector.keyPEM)) (empty .Values.tapInjector.keyPEM) }} +--- +{{- end }} +{{- include "linkerd.webhook.validation" .Values.tapInjector }} +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: linkerd-tap-injector-webhook-config + {{- if or (.Values.tapInjector.injectCaFrom) (.Values.tapInjector.injectCaFromSecret) }} + annotations: + {{- if .Values.tapInjector.injectCaFrom }} + cert-manager.io/inject-ca-from: {{ .Values.tapInjector.injectCaFrom }} + {{- end }} + {{- if .Values.tapInjector.injectCaFromSecret }} + cert-manager.io/inject-ca-from-secret: {{ .Values.tapInjector.injectCaFromSecret }} + {{- end }} + {{- end }} + labels: + linkerd.io/extension: viz + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +webhooks: +- name: tap-injector.linkerd.io + {{- if .Values.tapInjector.namespaceSelector }} + namespaceSelector: +{{ toYaml .Values.tapInjector.namespaceSelector | trim | indent 4 -}} + {{- end }} + {{- if .Values.tapInjector.objectSelector }} + objectSelector: +{{ toYaml .Values.tapInjector.objectSelector | trim | indent 4 -}} + {{- end }} + clientConfig: + service: + name: tap-injector + namespace: {{ .Release.Namespace }} + path: "/" + {{- if and (empty .Values.tapInjector.injectCaFrom) (empty .Values.tapInjector.injectCaFromSecret) }} + caBundle: {{ ternary (b64enc (trim $ca.Cert)) (b64enc (trim .Values.tapInjector.caBundle)) (empty .Values.tapInjector.caBundle) }} + {{- end }} + failurePolicy: {{.Values.tapInjector.failurePolicy}} + admissionReviewVersions: ["v1", "v1beta1"] + reinvocationPolicy: IfNeeded + rules: + - operations: [ "CREATE" ] + apiGroups: [""] + apiVersions: ["v1"] + resources: ["pods"] + sideEffects: None diff --git a/linkerd/charts/linkerd-viz/templates/tap-injector.yaml b/linkerd/charts/linkerd-viz/templates/tap-injector.yaml new file mode 100644 index 0000000..26fcc3f --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/tap-injector.yaml @@ -0,0 +1,129 @@ +--- +### +### Tap Injector +### +kind: Service +apiVersion: v1 +metadata: + name: tap-injector + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/extension: viz + component: tap-injector + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} + {{- with .Values.tapInjector.service.annotations }}{{ toYaml . | trim | nindent 4 }}{{- end }} + linkerd.io/inject: enabled +spec: + type: ClusterIP + selector: + linkerd.io/extension: viz + component: tap-injector + ports: + - name: tap-injector + port: 443 + targetPort: tap-injector +--- +{{- $tree := deepCopy . }} +kind: Deployment +apiVersion: apps/v1 +metadata: + annotations: + {{ include "partials.annotations.created-by" . }} + linkerd.io/inject: enabled + config.linkerd.io/proxy-await: "enabled" + labels: + linkerd.io/extension: viz + app.kubernetes.io/name: tap-injector + app.kubernetes.io/part-of: Linkerd + component: tap-injector + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + name: tap-injector + namespace: {{ .Release.Namespace }} +spec: + replicas: {{.Values.tapInjector.replicas}} + selector: + matchLabels: + component: tap-injector + {{- if .Values.enablePodAntiAffinity }} + strategy: + rollingUpdate: + maxUnavailable: 1 + {{- end }} + template: + metadata: + annotations: + {{- if empty .Values.cliVersion }} + checksum/config: {{ include (print $.Template.BasePath "/tap-injector-rbac.yaml") . | sha256sum }} + {{- end }} + {{ include "partials.annotations.created-by" . }} + {{- with .Values.tapInjector.proxy }} + {{- include "partials.proxy.config.annotations" .resources | nindent 8 }} + {{- end }} + {{- with .Values.podAnnotations }}{{ toYaml . | trim | nindent 8 }}{{- end }} + linkerd.io/inject: enabled + config.alpha.linkerd.io/proxy-wait-before-exit-seconds: "0" + cluster-autoscaler.kubernetes.io/safe-to-evict: "true" + labels: + linkerd.io/extension: viz + component: tap-injector + {{- with .Values.podLabels }}{{ toYaml . | trim | nindent 8 }}{{- end }} + spec: + {{- if .Values.tolerations -}} + {{- include "linkerd.tolerations" . | nindent 6 }} + {{- end -}} + {{- include "linkerd.node-selector" . | nindent 6 }} + {{- $_ := set $tree "component" "tap-injector" -}} + {{- $_ := set $tree "label" "component" -}} + {{- include "linkerd.affinity" $tree | nindent 6 }} + containers: + - args: + - injector + - -tap-service-name=tap.{{.Release.Namespace}}.serviceaccount.identity.{{.Values.linkerdNamespace}}.{{.Values.identityTrustDomain | default .Values.clusterDomain}} + - -log-level={{.Values.tapInjector.logLevel | default .Values.defaultLogLevel}} + - -log-format={{.Values.tapInjector.logFormat | default .Values.defaultLogFormat}} + - -enable-pprof={{.Values.enablePprof | default false}} + image: {{.Values.tapInjector.image.registry | default .Values.defaultRegistry}}/{{.Values.tapInjector.image.name}}:{{.Values.tapInjector.image.tag | default .Values.linkerdVersion}} + imagePullPolicy: {{.Values.tapInjector.image.pullPolicy | default .Values.defaultImagePullPolicy}} + livenessProbe: + httpGet: + path: /ping + port: 9995 + initialDelaySeconds: 10 + name: tap-injector + ports: + - containerPort: 8443 + name: tap-injector + - containerPort: 9995 + name: admin-http + readinessProbe: + failureThreshold: 7 + httpGet: + path: /ready + port: 9995 + {{- if .Values.tapInjector.resources -}} + {{- include "partials.resources" .Values.tapInjector.resources | nindent 8 }} + {{- end }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: {{.Values.tapInjector.UID | default .Values.defaultUID}} + seccompProfile: + type: RuntimeDefault + volumeMounts: + - mountPath: /var/run/linkerd/tls + name: tls + readOnly: true + securityContext: + seccompProfile: + type: RuntimeDefault + serviceAccountName: tap-injector + volumes: + - name: tls + secret: + secretName: tap-injector-k8s-tls diff --git a/linkerd/charts/linkerd-viz/templates/tap-policy.yaml b/linkerd/charts/linkerd-viz/templates/tap-policy.yaml new file mode 100644 index 0000000..8c4d0e8 --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/tap-policy.yaml @@ -0,0 +1,40 @@ +--- +apiVersion: policy.linkerd.io/v1beta1 +kind: Server +metadata: + namespace: {{ .Release.Namespace }} + name: tap-api + labels: + linkerd.io/extension: viz + component: tap + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + podSelector: + matchLabels: + linkerd.io/extension: viz + component: tap + port: apiserver + proxyProtocol: TLS +--- +apiVersion: policy.linkerd.io/v1alpha1 +kind: AuthorizationPolicy +metadata: + namespace: {{ .Release.Namespace }} + name: tap + labels: + linkerd.io/extension: viz + component: tap + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +spec: + targetRef: + group: policy.linkerd.io + kind: Server + name: tap-api + requiredAuthenticationRefs: + - group: policy.linkerd.io + kind: NetworkAuthentication + name: kube-api-server diff --git a/linkerd/charts/linkerd-viz/templates/tap-rbac.yaml b/linkerd/charts/linkerd-viz/templates/tap-rbac.yaml new file mode 100644 index 0000000..a4cd2ba --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/tap-rbac.yaml @@ -0,0 +1,154 @@ +--- +### +### Tap RBAC +### +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: linkerd-{{.Release.Namespace}}-tap + labels: + linkerd.io/extension: viz + component: tap + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +rules: +- apiGroups: [""] + resources: ["pods", "services", "replicationcontrollers", "namespaces", "nodes"] + verbs: ["list", "get", "watch"] +- apiGroups: ["extensions", "apps"] + resources: ["daemonsets", "deployments", "replicasets", "statefulsets"] + verbs: ["list", "get", "watch"] +- apiGroups: ["extensions", "batch"] + resources: ["cronjobs", "jobs"] + verbs: ["list" , "get", "watch"] +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: linkerd-{{.Release.Namespace}}-tap-admin + labels: + linkerd.io/extension: viz + component: tap + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +rules: +- apiGroups: [""] + resources: ["namespaces"] + verbs: ["list"] +- apiGroups: ["tap.linkerd.io"] + resources: ["*"] + verbs: ["watch"] +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: linkerd-{{.Release.Namespace}}-tap + labels: + linkerd.io/extension: viz + component: tap + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: linkerd-{{.Release.Namespace}}-tap +subjects: +- kind: ServiceAccount + name: tap + namespace: {{.Release.Namespace}} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: linkerd-{{.Release.Namespace}}-tap-auth-delegator + labels: + linkerd.io/extension: viz + component: tap + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:auth-delegator +subjects: +- kind: ServiceAccount + name: tap + namespace: {{.Release.Namespace}} +--- +kind: ServiceAccount +apiVersion: v1 +metadata: + name: tap + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/extension: viz + component: tap + namespace: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +{{- include "partials.image-pull-secrets" .Values.imagePullSecrets }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: linkerd-{{.Release.Namespace}}-tap-auth-reader + namespace: kube-system + labels: + linkerd.io/extension: viz + component: tap + namespace: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: extension-apiserver-authentication-reader +subjects: +- kind: ServiceAccount + name: tap + namespace: {{.Release.Namespace}} +--- +{{- $host := printf "tap.%s.svc" .Release.Namespace }} +{{- $ca := genSelfSignedCert $host (list) (list $host) 365 }} +{{- if (not .Values.tap.externalSecret) }} +kind: Secret +apiVersion: v1 +metadata: + name: tap-k8s-tls + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/extension: viz + component: tap + namespace: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} +type: kubernetes.io/tls +data: + tls.crt: {{ ternary (b64enc (trim $ca.Cert)) (b64enc (trim .Values.tap.crtPEM)) (empty .Values.tap.crtPEM) }} + tls.key: {{ ternary (b64enc (trim $ca.Key)) (b64enc (trim .Values.tap.keyPEM)) (empty .Values.tap.keyPEM) }} +--- +{{- end }} +{{- include "linkerd.webhook.validation" .Values.tap }} +apiVersion: apiregistration.k8s.io/v1 +kind: APIService +metadata: + name: v1alpha1.tap.linkerd.io + {{- if or (.Values.tap.injectCaFrom) (.Values.tap.injectCaFromSecret) }} + annotations: + {{- if .Values.tap.injectCaFrom }} + cert-manager.io/inject-ca-from: {{ .Values.tap.injectCaFrom }} + {{- end }} + {{- if .Values.tap.injectCaFromSecret }} + cert-manager.io/inject-ca-from-secret: {{ .Values.tap.injectCaFromSecret }} + {{- end }} + {{- end }} + labels: + linkerd.io/extension: viz + component: tap + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +spec: + group: tap.linkerd.io + version: v1alpha1 + groupPriorityMinimum: 1000 + versionPriority: 100 + service: + name: tap + namespace: {{.Release.Namespace}} + {{- if and (empty .Values.tap.injectCaFrom) (empty .Values.tap.injectCaFromSecret) }} + caBundle: {{ ternary (b64enc (trim $ca.Cert)) (b64enc (trim .Values.tap.caBundle)) (empty .Values.tap.caBundle) }} + {{- end }} diff --git a/linkerd/charts/linkerd-viz/templates/tap.yaml b/linkerd/charts/linkerd-viz/templates/tap.yaml new file mode 100644 index 0000000..5f0ccbe --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/tap.yaml @@ -0,0 +1,144 @@ +--- +### +### Tap +### +kind: Service +apiVersion: v1 +metadata: + name: tap + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/extension: viz + component: tap + namespace: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} + {{- with .Values.tap.service.annotations }}{{ toYaml . | trim | nindent 4 }}{{- end }} + linkerd.io/inject: enabled +spec: + type: ClusterIP + selector: + linkerd.io/extension: viz + component: tap + ports: + - name: grpc + port: 8088 + targetPort: 8088 + - name: apiserver + port: 443 + targetPort: apiserver +--- +{{- $tree := deepCopy . }} +kind: Deployment +apiVersion: apps/v1 +metadata: + annotations: + {{ include "partials.annotations.created-by" . }} + linkerd.io/inject: enabled + config.linkerd.io/proxy-await: "enabled" + labels: + linkerd.io/extension: viz + app.kubernetes.io/name: tap + app.kubernetes.io/part-of: Linkerd + app.kubernetes.io/version: {{default .Values.linkerdVersion .Values.cliVersion}} + component: tap + namespace: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + name: tap + namespace: {{ .Release.Namespace }} +spec: + replicas: {{.Values.tap.replicas}} + selector: + matchLabels: + linkerd.io/extension: viz + component: tap + namespace: {{.Release.Namespace}} + {{- if .Values.enablePodAntiAffinity }} + strategy: + rollingUpdate: + maxUnavailable: 1 + {{- end }} + template: + metadata: + annotations: + {{- if empty .Values.cliVersion }} + checksum/config: {{ include (print $.Template.BasePath "/tap-rbac.yaml") . | sha256sum }} + {{- end }} + {{ include "partials.annotations.created-by" . }} + {{- with .Values.tap.proxy }} + {{- include "partials.proxy.config.annotations" .resources | nindent 8 }} + {{- end }} + {{- with .Values.podAnnotations }}{{ toYaml . | trim | nindent 8 }}{{- end }} + linkerd.io/inject: enabled + config.alpha.linkerd.io/proxy-wait-before-exit-seconds: "0" + cluster-autoscaler.kubernetes.io/safe-to-evict: "true" + labels: + linkerd.io/extension: viz + component: tap + namespace: {{.Release.Namespace}} + {{- with .Values.podLabels }}{{ toYaml . | trim | nindent 8 }}{{- end }} + spec: + {{- if .Values.tolerations -}} + {{- include "linkerd.tolerations" . | nindent 6 }} + {{- end -}} + {{- include "linkerd.node-selector" . | nindent 6 }} + {{- $_ := set $tree "component" "tap" -}} + {{- $_ := set $tree "label" "component" -}} + {{- include "linkerd.affinity" $tree | nindent 6 }} + containers: + - args: + - api + - -api-namespace={{.Values.linkerdNamespace}} + - -log-level={{.Values.tap.logLevel | default .Values.defaultLogLevel}} + - -log-format={{.Values.tap.logFormat | default .Values.defaultLogFormat}} + - -identity-trust-domain={{.Values.identityTrustDomain | default .Values.clusterDomain}} + - -enable-pprof={{.Values.enablePprof | default false}} + {{- if .Values.tap.ignoreHeaders }} + - -ignore-headers={{ .Values.tap.ignoreHeaders | join "," }} + {{- end }} + image: {{.Values.tap.image.registry | default .Values.defaultRegistry}}/{{.Values.tap.image.name}}:{{.Values.tap.image.tag | default .Values.linkerdVersion}} + imagePullPolicy: {{.Values.tap.image.pullPolicy | default .Values.defaultImagePullPolicy}} + livenessProbe: + httpGet: + path: /ping + port: 9998 + initialDelaySeconds: 10 + name: tap + ports: + - containerPort: 8088 + name: grpc + - containerPort: 8089 + name: apiserver + - containerPort: 9998 + name: admin-http + readinessProbe: + failureThreshold: 7 + httpGet: + path: /ready + port: 9998 + {{- if .Values.tap.resources -}} + {{- include "partials.resources" .Values.tap.resources | nindent 8 }} + {{- end }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: {{.Values.tap.UID | default .Values.defaultUID}} + seccompProfile: + type: RuntimeDefault + volumeMounts: + - mountPath: /var/run/linkerd/tls + name: tls + readOnly: true + securityContext: + seccompProfile: + type: RuntimeDefault + serviceAccountName: tap + volumes: + - name: tls + secret: + secretName: tap-k8s-tls diff --git a/linkerd/charts/linkerd-viz/templates/web-rbac.yaml b/linkerd/charts/linkerd-viz/templates/web-rbac.yaml new file mode 100644 index 0000000..c7d7329 --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/web-rbac.yaml @@ -0,0 +1,155 @@ +--- +### +### Web RBAC +### +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: web + namespace: {{.Values.linkerdNamespace}} + labels: + linkerd.io/extension: viz + component: web + namespace: {{.Values.linkerdNamespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +rules: +- apiGroups: [""] + resources: ["configmaps"] + verbs: ["get"] + resourceNames: ["linkerd-config"] + {{- if not .Values.dashboard.restrictPrivileges }} +- apiGroups: [""] + resources: ["namespaces", "configmaps"] + verbs: ["get"] +- apiGroups: [""] + resources: ["serviceaccounts", "pods"] + verbs: ["list"] +- apiGroups: ["apps"] + resources: ["replicasets"] + verbs: ["list"] + {{- end }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: web + namespace: {{.Values.linkerdNamespace}} + labels: + linkerd.io/extension: viz + component: web + namespace: {{.Values.linkerdNamespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +roleRef: + kind: Role + name: web + apiGroup: rbac.authorization.k8s.io +subjects: +- kind: ServiceAccount + name: web + namespace: {{.Release.Namespace}} +--- +{{- if not .Values.dashboard.restrictPrivileges }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: linkerd-{{.Release.Namespace}}-web-check + labels: + linkerd.io/extension: viz + component: web + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +rules: +- apiGroups: ["rbac.authorization.k8s.io"] + resources: ["clusterroles", "clusterrolebindings"] + verbs: ["list"] +- apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["list"] +- apiGroups: ["admissionregistration.k8s.io"] + resources: ["mutatingwebhookconfigurations", "validatingwebhookconfigurations"] + verbs: ["list"] +- apiGroups: ["linkerd.io"] + resources: ["serviceprofiles"] + verbs: ["list"] +- apiGroups: [""] + resources: ["nodes", "pods", "services"] + verbs: ["list"] +- apiGroups: ["apiregistration.k8s.io"] + resources: ["apiservices"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: linkerd-{{.Release.Namespace}}-web-check + labels: + linkerd.io/extension: viz + component: web + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +roleRef: + kind: ClusterRole + name: linkerd-{{.Release.Namespace}}-web-check + apiGroup: rbac.authorization.k8s.io +subjects: +- kind: ServiceAccount + name: web + namespace: {{.Release.Namespace}} +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: linkerd-{{.Release.Namespace}}-web-admin + labels: + linkerd.io/extension: viz + component: web + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: linkerd-{{.Release.Namespace}}-tap-admin +subjects: +- kind: ServiceAccount + name: web + namespace: {{.Release.Namespace}} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: linkerd-{{.Release.Namespace}}-web-api + labels: + linkerd.io/extension: viz + component: web + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +rules: +- apiGroups: [""] + resources: ["namespaces"] + verbs: ["list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: linkerd-{{.Release.Namespace}}-web-api + labels: + linkerd.io/extension: viz + component: web + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +roleRef: + kind: ClusterRole + name: linkerd-{{.Release.Namespace}}-web-api + apiGroup: rbac.authorization.k8s.io +subjects: +- kind: ServiceAccount + name: web + namespace: {{.Release.Namespace}} +--- +{{- end}} +kind: ServiceAccount +apiVersion: v1 +metadata: + name: web + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/extension: viz + component: web + namespace: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} +{{- include "partials.image-pull-secrets" .Values.imagePullSecrets }} diff --git a/linkerd/charts/linkerd-viz/templates/web.yaml b/linkerd/charts/linkerd-viz/templates/web.yaml new file mode 100644 index 0000000..95ba27d --- /dev/null +++ b/linkerd/charts/linkerd-viz/templates/web.yaml @@ -0,0 +1,141 @@ +--- +### +### Web +### +kind: Service +apiVersion: v1 +metadata: + name: web + namespace: {{ .Release.Namespace }} + labels: + linkerd.io/extension: viz + component: web + namespace: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + annotations: + {{ include "partials.annotations.created-by" . }} + {{ with .Values.dashboard.service.annotations }}{{ toYaml . | trim | nindent 4 }}{{ end }} + linkerd.io/inject: enabled +spec: + type: ClusterIP + selector: + linkerd.io/extension: viz + component: web + ports: + - name: http + port: 8084 + targetPort: 8084 + - name: admin-http + port: 9994 + targetPort: 9994 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + {{ include "partials.annotations.created-by" . }} + linkerd.io/inject: enabled + config.linkerd.io/proxy-await: "enabled" + labels: + linkerd.io/extension: viz + app.kubernetes.io/name: web + app.kubernetes.io/part-of: Linkerd + app.kubernetes.io/version: {{default .Values.linkerdVersion .Values.cliVersion}} + component: web + namespace: {{.Release.Namespace}} + {{- with .Values.commonLabels }}{{ toYaml . | trim | nindent 4 }}{{- end }} + name: web + namespace: {{ .Release.Namespace }} +spec: + replicas: {{.Values.dashboard.replicas}} + selector: + matchLabels: + linkerd.io/extension: viz + component: web + namespace: {{.Release.Namespace}} + template: + metadata: + annotations: + {{ include "partials.annotations.created-by" . }} + {{- with .Values.dashboard.proxy }} + {{- include "partials.proxy.config.annotations" .resources | nindent 8 }} + {{- end }} + {{- with .Values.podAnnotations }}{{ toYaml . | trim | nindent 8 }}{{- end }} + linkerd.io/inject: enabled + config.alpha.linkerd.io/proxy-wait-before-exit-seconds: "0" + cluster-autoscaler.kubernetes.io/safe-to-evict: "true" + labels: + linkerd.io/extension: viz + component: web + namespace: {{.Release.Namespace}} + {{- with .Values.podLabels }}{{ toYaml . | trim | nindent 8 }}{{- end }} + spec: + {{- if .Values.tolerations -}} + {{- include "linkerd.tolerations" . | nindent 6 }} + {{- end -}} + {{- include "linkerd.node-selector" . | nindent 6 }} + containers: + - args: + - -linkerd-metrics-api-addr=metrics-api.{{.Release.Namespace}}.svc.{{.Values.clusterDomain}}:8085 + - -cluster-domain={{.Values.clusterDomain}} + {{- if and .Values.grafana.url .Values.grafana.externalUrl }} + {{ fail "Cannot set both grafana.url (on-cluster Grafana) and grafana.externalUrl (off-cluster Grafana)"}} + {{- end}} + {{- if .Values.grafana.url }} + - -grafana-addr={{.Values.grafana.url}} + {{- end}} + {{- if .Values.grafana.externalUrl }} + - -grafana-external-addr={{.Values.grafana.externalUrl}} + {{- end}} + {{- if .Values.grafana.uidPrefix }} + - -grafana-prefix={{.Values.grafana.uidPrefix}} + {{- end}} + {{- if .Values.jaegerUrl }} + - -jaeger-addr={{.Values.jaegerUrl}} + {{- end}} + - -controller-namespace={{.Values.linkerdNamespace}} + - -log-level={{.Values.dashboard.logLevel | default .Values.defaultLogLevel}} + - -log-format={{.Values.dashboard.logFormat | default .Values.defaultLogFormat}} + {{- if .Values.dashboard.enforcedHostRegexp }} + - -enforced-host={{.Values.dashboard.enforcedHostRegexp}} + {{- else -}} + {{- $hostFull := replace "." "\\." (printf "web.%s.svc.%s" .Release.Namespace .Values.clusterDomain) }} + {{- $hostAbbrev := replace "." "\\." (printf "web.%s.svc" .Release.Namespace) }} + - -enforced-host=^(localhost|127\.0\.0\.1|{{ $hostFull }}|{{ $hostAbbrev }}|\[::1\])(:\d+)?$ + {{- end}} + - -enable-pprof={{.Values.enablePprof | default false}} + image: {{.Values.dashboard.image.registry | default .Values.defaultRegistry}}/{{.Values.dashboard.image.name}}:{{.Values.dashboard.image.tag | default .Values.linkerdVersion}} + imagePullPolicy: {{.Values.dashboard.image.pullPolicy | default .Values.defaultImagePullPolicy}} + livenessProbe: + httpGet: + path: /ping + port: 9994 + initialDelaySeconds: 10 + name: web + ports: + - containerPort: 8084 + name: http + - containerPort: 9994 + name: admin-http + readinessProbe: + failureThreshold: 7 + httpGet: + path: /ready + port: 9994 + {{- if .Values.dashboard.resources -}} + {{- include "partials.resources" .Values.dashboard.resources | nindent 8 }} + {{- end }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: {{.Values.dashboard.UID | default .Values.defaultUID}} + seccompProfile: + type: RuntimeDefault + securityContext: + seccompProfile: + type: RuntimeDefault + serviceAccountName: web diff --git a/linkerd/charts/linkerd-viz/values-ha.yaml b/linkerd/charts/linkerd-viz/values-ha.yaml new file mode 100644 index 0000000..65ebcc4 --- /dev/null +++ b/linkerd/charts/linkerd-viz/values-ha.yaml @@ -0,0 +1,35 @@ +# This values.yaml file contains the values needed to enable HA mode. +# Usage: +# helm install -f values.yaml -f values-ha.yaml + +enablePodAntiAffinity: true + +# nodeAffinity: + +resources: &ha_resources + cpu: &ha_resources_cpu + limit: "" + request: 100m + memory: + limit: 250Mi + request: 50Mi + + +# tap configuration +tap: + replicas: 3 + resources: *ha_resources + +# web configuration +dashboard: + resources: *ha_resources + +# prometheus configuration +prometheus: + resources: + cpu: + limit: "" + request: 300m + memory: + limit: 8192Mi + request: 300Mi diff --git a/linkerd/charts/linkerd-viz/values.yaml b/linkerd/charts/linkerd-viz/values.yaml new file mode 100644 index 0000000..06c683d --- /dev/null +++ b/linkerd/charts/linkerd-viz/values.yaml @@ -0,0 +1,554 @@ +# Default values for linkerd. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# Fields that should be common with the core control plane + +# -- control plane version. See Proxy section for proxy version +linkerdVersion: stable-2.14.10 +# -- Kubernetes DNS Domain name to use +clusterDomain: cluster.local +# -- Additional labels to add to all pods +podLabels: {} +# -- Labels to apply to all resources +commonLabels: {} +# -- Trust domain used for identity +# @default -- clusterDomain +identityTrustDomain: "" + +# -- Docker registry for all viz components +defaultRegistry: cr.l5d.io/linkerd +# -- Docker imagePullPolicy for all viz components +defaultImagePullPolicy: IfNotPresent +# -- Log level for all the viz components +defaultLogLevel: info +# -- Log format (`plain` or `json`) for all the viz components. +defaultLogFormat: plain +# -- UID for all the viz components +defaultUID: 2103 + +# -- Namespace of the Linkerd core control-plane install +linkerdNamespace: linkerd + +# -- Default nodeSelector section, See the +# [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information +nodeSelector: &default_node_selector + kubernetes.io/os: linux + +# -- For Private docker registries, authentication is needed. +# Registry secrets are applied to the respective service accounts +imagePullSecrets: [] +# - name: my-private-docker-registry-login-secret + +# -- Default tolerations section, See the +# [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) +# for more information +tolerations: &default_tolerations + +# -- Enables Pod Anti Affinity logic to balance the placement of replicas +# across hosts and zones for High Availability. +# Enable this only when you have multiple replicas of components. +enablePodAntiAffinity: false + +# -- NodeAffinity section, See the +# [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity) +# for more information +# nodeAffinity: + +# -- Create Roles and RoleBindings to associate this extension's +# ServiceAccounts to the control plane PSP resource. This requires that +# `enabledPSP` is set to true on the control plane install. Note PSP has been +# deprecated since k8s v1.21 +enablePSP: false + +# -- url of external prometheus instance +prometheusUrl: "" + +# -- url of external jaeger instance +# Set this to `jaeger.linkerd-jaeger.svc.:16686` if you plan to use jaeger extension +jaegerUrl: "" + +# metrics API configuration +metricsAPI: + # -- number of replicas of the metrics-api component + replicas: 1 + # -- log level of the metrics-api component + # @default -- defaultLogLevel + logLevel: "" + # -- log format of the metrics-api component + # @default -- defaultLogFormat + logFormat: "" + image: + # -- Docker registry for the metrics-api component + # @default -- defaultRegistry + registry: "" + # -- Docker image name for the metrics-api component + name: metrics-api + # -- Docker image tag for the metrics-api component + # @default -- linkerdVersion + tag: "" + # -- Pull policy for the metrics-api component + # @default -- defaultImagePullPolicy + pullPolicy: "" + + resources: + cpu: + # -- Maximum amount of CPU units that the metrics-api container can use + limit: + # -- Amount of CPU units that the metrics-api container requests + request: + memory: + # -- Maximum amount of memory that metrics-api container can use + limit: + # -- Amount of memory that the metrics-api container requests + request: + ephemeral-storage: + # -- Maximum amount of ephemeral storage that the metrics-api container can use + limit: "" + # -- Amount of ephemeral storage that the metrics-api container requests + request: "" + + proxy: + # -- If set, overrides default proxy resources for the proxy injected + # into the metrics-api component + # resources: + + # -- UID for the metrics-api resource + UID: + + # -- NodeSelector section, See the + # [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information + nodeSelector: *default_node_selector + # -- Tolerations section, See the + # [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) + # for more information + tolerations: *default_tolerations + + # -- metrics-api service configuration + service: + # -- Additional annotations to add to metrics-api service + annotations: {} + +# tap configuration +tap: + # -- Number of tap component replicas + replicas: 1 + # -- log level of the tap component + # @default -- defaultLogLevel + logLevel: "" + # -- log format of the tap component + # @default -- defaultLogFormat + logFormat: "" + image: + # -- Docker registry for the tap instance + # @default -- defaultRegistry + registry: "" + # -- Docker image name for the tap instance + name: tap + # -- Docker image tag for the tap instance + # @default -- linkerdVersion + tag: "" + # -- Pull policy for the tap component + # @default -- defaultImagePullPolicy + pullPolicy: "" + + # -- Do not create a secret resource for the Tap component. + # If this is set to `true`, the value `tap.caBundle` must be set + # or the ca bundle must injected with cert-manager ca injector using + # `tap.injectCaFrom` or `tap.injectCaFromSecret` (see below). + externalSecret: false + + # -- Certificate for the Tap component. If not provided and not using an external secret + # then Helm will generate one. + crtPEM: | + + # -- Certificate key for Tap component. If not provided and not using an external secret + # then Helm will generate one. + keyPEM: | + + # -- Bundle of CA certificates for tap. + # If not provided nor injected with cert-manager, + # then Helm will use the certificate generated for `tap.crtPEM`. + # If `tap.externalSecret` is set to true, this value, injectCaFrom, or + # injectCaFromSecret must be set, as no certificate will be generated. + # See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector) for more information. + caBundle: | + + # -- Inject the CA bundle from a cert-manager Certificate. + # See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-certificate-resource) + # for more information. + injectCaFrom: "" + + # -- Inject the CA bundle from a Secret. + # If set, the `cert-manager.io/inject-ca-from-secret` annotation will be added to the webhook. + # The Secret must have the CA Bundle stored in the `ca.crt` key and have + # the `cert-manager.io/allow-direct-injection` annotation set to `true`. + # See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-secret-resource) + # for more information. + injectCaFromSecret: "" + + resources: + cpu: + # -- Maximum amount of CPU units that the tap container can use + limit: + # -- Amount of CPU units that the tap container requests + request: + memory: + # -- Maximum amount of memory that tap container can use + limit: + # -- Amount of memory that the tap container requests + request: + ephemeral-storage: + # -- Maximum amount of ephemeral storage that the tap container can use + limit: "" + # -- Amount of ephemeral storage that the tap container requests + request: "" + + # -- List of headers that will be ignored for Linkerd Tap + ignoreHeaders: [] + + proxy: + # -- If set, overrides default proxy resources for the proxy injected + # into the tap component + # resources: + + # -- UID for the dashboard resource + UID: + + # -- tap service configuration + service: + # -- Additional annotations to add to tap service + annotations: {} + +# tapInjector configuration +tapInjector: + # -- Number of replicas of tapInjector + replicas: 1 + # -- log level of the tapInjector + # @default -- defaultLogLevel + logLevel: "" + # -- log format of the tapInjector component + # @default -- defaultLogFormat + logFormat: "" + image: + # -- Docker registry for the tapInjector instance + # @default -- defaultRegistry + registry: "" + # -- Docker image name for the tapInjector instance + name: tap + # -- Docker image tag for the tapInjector instance + # @default -- linkerdVersion + tag: "" + # -- Pull policy for the tapInjector component + # @default -- defaultImagePullPolicy + pullPolicy: "" + + namespaceSelector: + # matchExpressions: + # - key: runlevel + # operator: NotIn + # values: ["0","1"] + objectSelector: + # matchLabels: + # foo: bar + # -- UID for the tapInjector resource + UID: + failurePolicy: Ignore + resources: + cpu: + # -- Maximum amount of CPU units that the tapInjector container can use + limit: + # -- Amount of CPU units that the tapInjector container requests + request: + memory: + # -- Maximum amount of memory that tapInjector container can use + limit: + # -- Amount of memory that the tapInjector container requests + request: + ephemeral-storage: + # -- Maximum amount of ephemeral storage that the tapInjector container can use + limit: "" + # -- Amount of ephemeral storage that the tapInjector container requests + request: "" + proxy: + # -- If set, overrides default proxy resources for the proxy injected + # into the tapInjector component + # resources: + + # -- Do not create a secret resource for the tapInjector webhook. + # If this is set to `true`, the value `tapInjector.caBundle` must be set + # or the ca bundle must injected with cert-manager ca injector using + # `tapInjector.injectCaFrom` or `tapInjector.injectCaFromSecret` (see below). + externalSecret: false + + # -- Certificate for the tapInjector. If not provided and not using an external secret + # then Helm will generate one. + crtPEM: | + + # -- Certificate key for the tapInjector. If not provided and not using an external secret + # then Helm will generate one. + keyPEM: | + + # -- Bundle of CA certificates for the tapInjector. + # If not provided nor injected with cert-manager, + # then Helm will use the certificate generated for `tapInjector.crtPEM`. + # If `tapInjector.externalSecret` is set to true, this value, injectCaFrom, or + # injectCaFromSecret must be set, as no certificate will be generated. + # See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector) for more information. + caBundle: | + + # -- Inject the CA bundle from a cert-manager Certificate. + # See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-certificate-resource) + # for more information. + injectCaFrom: "" + + # -- Inject the CA bundle from a Secret. + # If set, the `cert-manager.io/inject-ca-from-secret` annotation will be added to the webhook. + # The Secret must have the CA Bundle stored in the `ca.crt` key and have + # the `cert-manager.io/allow-direct-injection` annotation set to `true`. + # See the cert-manager [CA Injector Docs](https://cert-manager.io/docs/concepts/ca-injector/#injecting-ca-data-from-a-secret-resource) + # for more information. + injectCaFromSecret: "" + + # -- tap service configuration + service: + # -- Additional annotations to add to tapInjector service + annotations: {} + +# web dashboard configuration +dashboard: + # -- Number of replicas of dashboard + replicas: 1 + # -- log level of the dashboard component + # @default -- defaultLogLevel + logLevel: "" + # -- log format of the dashboard component + # @default -- defaultLogFormat + logFormat: "" + image: + # -- Docker registry for the web instance + # @default -- defaultRegistry + registry: "" + # -- Docker image name for the web instance + name: web + # -- Docker image tag for the web instance + # @default -- linkerdVersion + tag: "" + # -- Pull policy for the web component + # @default -- defaultImagePullPolicy + pullPolicy: "" + + # -- UID for the dashboard resource + UID: + + # -- Restrict the Linkerd Dashboard's default privileges to disallow Tap and Check + restrictPrivileges: false + + # -- Host header validation regex for the dashboard. See the [Linkerd + # documentation](https://linkerd.io/2/tasks/exposing-dashboard) for more + # information + enforcedHostRegexp: "" + resources: + cpu: + # -- Maximum amount of CPU units that the web container can use + limit: + # -- Amount of CPU units that the web container requests + request: + memory: + # -- Maximum amount of memory that web container can use + limit: + # -- Amount of memory that the web container requests + request: + ephemeral-storage: + # -- Maximum amount of ephemeral storage that the web container can use + limit: "" + # -- Amount of ephemeral storage that the web container requests + request: "" + + proxy: + # -- If set, overrides default proxy resources for the proxy injected + # into the dashboard component + # resources: + + # -- dashboard service configuration + service: + # -- Additional annotations to add to dashboard service + annotations: {} + +namespaceMetadata: + image: + # -- Docker registry for the namespace-metadata instance + # @default -- defaultRegistry + registry: "" + # -- Docker image name for the namespace-metadata instance + name: extension-init + # -- Docker image tag for the namespace-metadata instance + tag: v0.1.0 + # -- Pull policy for the namespace-metadata instance + # @default -- defaultImagePullPolicy + pullPolicy: "" + + # -- NodeSelector section, See the + # [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information + nodeSelector: *default_node_selector + # -- Tolerations section, See the + # [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) + # for more information + tolerations: *default_tolerations + +grafana: + # -- url of an in-cluster Grafana instance with reverse proxy configured, used by the + # Linkerd viz web dashboard to provide direct links to specific Grafana + # dashboards. Cannot be set if grafana.externalUrl is set. See the [Linkerd + # documentation](https://linkerd.io/2/tasks/grafana) for more information + url: + # -- url of a Grafana instance hosted off-cluster. Cannot be set if + # grafana.url is set. The reverse proxy will not be used for this URL. + externalUrl: + # -- prefix for Grafana dashboard UID's, used when grafana.externalUrl is + # set. + uidPrefix: + +prometheus: + # -- toggle field to enable or disable prometheus + enabled: true + image: + # -- Docker registry for the prometheus instance + registry: prom + # -- Docker image name for the prometheus instance + name: prometheus + # -- Docker image tag for the prometheus instance + tag: v2.48.0 + # -- Pull policy for the prometheus instance + # @default -- defaultImagePullPolicy + pullPolicy: "" + + # -- log level of the prometheus instance + # @default -- defaultLogLevel + logLevel: "" + # -- log format (plain, json) of the prometheus instance + # @default -- defaultLogLevel + logFormat: "" + # -- Command line options for Prometheus binary + args: + storage.tsdb.path: /data + storage.tsdb.retention.time: 6h + config.file: /etc/prometheus/prometheus.yml + # -- The global configuration specifies parameters that are valid in all other + # configuration contexts. + globalConfig: + scrape_interval: 10s + scrape_timeout: 10s + evaluation_interval: 10s + + # -- Alert relabeling is applied to alerts before they are sent to the + # Alertmanager. + alertRelabelConfigs: + # Ex: + # - action: labeldrop + # regex: prometheus_replica + + # -- Alertmanager instances the Prometheus server sends alerts to configured via + # the static_configs parameter. + alertmanagers: + # Ex: + # - scheme: http + # static_configs: + # - targets: + # - "alertmanager.linkerd.svc:9093" + + # -- Allows transparently sending samples to an endpoint. Mostly used for long + # term storage. + remoteWrite: + + # -- Alerting/recording rule ConfigMap mounts (sub-path names must end in + # ´_rules.yml´ or ´_rules.yaml´) + ruleConfigMapMounts: + # Ex: + # - name: alerting-rules + # subPath: alerting_rules.yml + # configMap: linkerd-prometheus-rules + # - name: recording-rules + # subPath: recording_rules.yml + # configMap: linkerd-prometheus-rules + + # -- A scrapeConfigs section specifies a set of targets and parameters + # describing how to scrape them. + scrapeConfigs: + # Ex: + # - job_name: 'kubernetes-nodes' + # scheme: https + # tls_config: + # ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt + # bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token + # kubernetes_sd_configs: + # - role: node + # relabel_configs: + # - action: labelmap + # regex: __meta_kubernetes_node_label_(.+) + + # -- A sidecarContainers section specifies a list of secondary containers to run + # in the prometheus pod e.g. to export data to non-prometheus systems + sidecarContainers: + # Ex: + # - name: sidecar + # image: gcr.io/myproject/stackdriver-prometheus-sidecar + # imagePullPolicy: Always + # command: + # - /bin/sh + # - -c + # - | + # exec /bin/stackdriver-prometheus-sidecar \ + # --stackdriver.project-id=myproject \ + # --stackdriver.kubernetes.location=us-central1 \ + # --stackdriver.kubernetes.cluster-name=mycluster \ + # --prometheus.wal-directory=/data/wal \ + # --log.level=info + # volumeMounts: + # - mountPath: /data + # name: data + # ports: + # - name: foo + # containerPort: 9091 + # protocol: TCP + ### WARNING: persistence is experimental and has not been tested/vetted by the Linkerd team. + ### As such, please refer to https://linkerd.io/2/tasks/exporting-metrics/ for the recommended approach to metrics data retention. + # if enabled, creates a persistent volume claim for prometheus data + # https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims + #persistence: + # -- Storage class used to create prometheus data PV. + # storageClass: + # -- PVC access mode. + # accessMode: + # -- Prometheus data volume size. + # size: + + resources: + cpu: + # -- Maximum amount of CPU units that the prometheus container can use + limit: + # -- Amount of CPU units that the prometheus container requests + request: + memory: + # -- Maximum amount of memory that prometheus container can use + limit: + # -- Amount of memory that the prometheus container requests + request: + ephemeral-storage: + # -- Maximum amount of ephemeral storage that the prometheus container can use + limit: "" + # -- Amount of ephemeral storage that the prometheus container requests + request: "" + + proxy: + # -- If set, overrides default proxy resources for the proxy injected + # into the prometheus component + # resources: + + # -- NodeSelector section, See the + # [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information + nodeSelector: *default_node_selector + # -- Tolerations section, See the + # [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) + # for more information + tolerations: *default_tolerations diff --git a/linkerd/control-plane.yaml b/linkerd/control-plane.yaml new file mode 100644 index 0000000..4f0a817 --- /dev/null +++ b/linkerd/control-plane.yaml @@ -0,0 +1,80 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +helmCharts: + - name: linkerd-control-plane + releaseName: linkerd-control-plane + #version: 1.16.11 + #version: 2026.3.3 # Replace with your target version + repo: https://helm.linkerd.io/stable + namespace: linkerd + # Values here replace the --set flags you used in the CLI + valuesInline: + identity: + issuer: + scheme: kubernetes.io/tls + disableHeartbeat: true + enableGatewayAPI: false + valuesFile: values-identity.yaml + + - name: linkerd-viz + releaseName: linkerd-viz + #version: 30.12.0 # Replace with your target version + repo: https://helm.linkerd.io/stable + namespace: linkerd-viz + +# You can manage the secret here instead of manual kubectl commands +secretGenerator: +- name: linkerd-identity-issuer + namespace: linkerd + type: kubernetes.io/tls + files: + - tls.crt=issuer.crt + - tls.key=issuer.key + +# Generators for the identityTrustAnchorsPEM +generatorOptions: + disableNameSuffixHash: true + +configMapGenerator: + - name: linkerd-ca-bundle + namespace: linkerd + files: + - ca.crt + +#THIS BLOCK RESOLVES THE MERGE ERROR +# patches: +# - target: +# group: apiextensions.k8s.io +# version: v1 +# kind: CustomResourceDefinition +# patch: |- +# # This tells K8s/Kustomize to "Replace" if it sees this ID twice +# $patch: replace +# metadata: +# annotations: +# kustomize.config.k8s.io/behavior: merge + # - target: + # group: apiextensions.k8s.io + # version: v1 + # kind: CustomResourceDefinition + # name: httproutes.gateway.networking.k8s.io + # patch: |- + # apiVersion: apiextensions.k8s.io/v1 + # kind: CustomResourceDefinition + # metadata: + # name: httproutes.gateway.networking.k8s.io + # - target: + # group: apiextensions.k8s.io + # version: v1 + # kind: CustomResourceDefinition + # name: gateways.gateway.networking.k8s.io + # patch: |- + # apiVersion: apiextensions.k8s.io/v1 + # kind: CustomResourceDefinition + # metadata: + # name: gateways.gateway.networking.k8s.io + + +# If you need to override the namespace for everything at once: +# namespace: apps \ No newline at end of file diff --git a/linkerd/issuer.crt b/linkerd/issuer.crt new file mode 100644 index 0000000..217e346 --- /dev/null +++ b/linkerd/issuer.crt @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBtjCCAVygAwIBAgIUdd/8fSGsrGBI7FrbZ1lA+jS489UwCgYIKoZIzj0EAwIw +JTEjMCEGA1UEAwwacm9vdC5saW5rZXJkLmNsdXN0ZXIubG9jYWwwHhcNMjYwMzEz +MjMwNTI2WhcNMjcwMzEzMjMwNTI2WjApMScwJQYDVQQDDB5pZGVudGl0eS5saW5r +ZXJkLmNsdXN0ZXIubG9jYWwwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQHU88u +R1qhnrxcDySFKz4BqLYs/fjGwvD77PqxLp37K23lzbM5UpXVB06cnCB03plrbxg3 +VzM4Y3CTU/OgCN0/o2YwZDASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQE +AwIBhjAdBgNVHQ4EFgQUc3Y9kn7kJpEqFvXxFtE302o289AwHwYDVR0jBBgwFoAU +1uwi63bm3tlO4ajCwZC2lUyfn+EwCgYIKoZIzj0EAwIDSAAwRQIhAPjSBYscyTfN +99dm/or5OxYSPIS6WbaySvHrfar+9ReCAiAMkdZ8P5IrGUERW2kKhzU6jWsPfFbw +ddI8y/PwVTDuRw== +-----END CERTIFICATE----- diff --git a/linkerd/issuer.csr b/linkerd/issuer.csr new file mode 100644 index 0000000..214d78e --- /dev/null +++ b/linkerd/issuer.csr @@ -0,0 +1,7 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIHjMIGLAgEAMCkxJzAlBgNVBAMMHmlkZW50aXR5LmxpbmtlcmQuY2x1c3Rlci5s +b2NhbDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAdTzy5HWqGevFwPJIUrPgGo +tiz9+MbC8Pvs+rEunfsrbeXNszlSldUHTpycIHTemWtvGDdXMzhjcJNT86AI3T+g +ADAKBggqhkjOPQQDAgNHADBEAiBZn9iGcXJR3TL03L94P5w7tEdlQbui8bgDxqiJ +x0lbQQIgf4cqLNGjkKwNTxuYTurH7RylFcNzdQGnlQnrF6p7bLg= +-----END CERTIFICATE REQUEST----- diff --git a/linkerd/issuer.ext b/linkerd/issuer.ext new file mode 100644 index 0000000..b98fa2b --- /dev/null +++ b/linkerd/issuer.ext @@ -0,0 +1,2 @@ +basicConstraints=critical,CA:TRUE,pathlen:0 +keyUsage=critical,digitalSignature,keyCertSign,cRLSign diff --git a/linkerd/issuer.key b/linkerd/issuer.key new file mode 100644 index 0000000..feaca97 --- /dev/null +++ b/linkerd/issuer.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIHxdoj94UpVxEjt+kNoLePmjiyRANZFpMVqQj2wAQdeyoAoGCCqGSM49 +AwEHoUQDQgAEB1PPLkdaoZ68XA8khSs+Aai2LP34xsLw++z6sS6d+ytt5c2zOVKV +1QdOnJwgdN6Za28YN1czOGNwk1PzoAjdPw== +-----END EC PRIVATE KEY----- diff --git a/linkerd/kustomization.yaml b/linkerd/kustomization.yaml new file mode 100644 index 0000000..190f436 --- /dev/null +++ b/linkerd/kustomization.yaml @@ -0,0 +1,97 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# Order matters: CRDs should be at the top +#resources: +#- ./linkerd-crds.yaml +# - linkerd-control-plane.yaml +# - linkerd-viz.yaml + + +helmCharts: + - name: linkerd-crds + releaseName: linkerd-crds + #version: 1.8.0 + #version: 2026.3.3 # Replace with your target version + repo: https://helm.linkerd.io/stable + namespace: linkerd + valuesInline: + installGatewayAPI: true + enableExperimental: true + + - name: linkerd-control-plane + releaseName: linkerd-control-plane + #version: 1.16.11 + #version: 2026.3.3 # Replace with your target version + repo: https://helm.linkerd.io/stable + namespace: linkerd + # Values here replace the --set flags you used in the CLI + valuesInline: + identity: + issuer: + scheme: kubernetes.io/tls + disableHeartbeat: true + enableGatewayAPI: false + valuesFile: values-identity.yaml + + - name: linkerd-viz + releaseName: linkerd-viz + #version: 30.12.0 # Replace with your target version + repo: https://helm.linkerd.io/stable + namespace: linkerd-viz + +# You can manage the secret here instead of manual kubectl commands +secretGenerator: +- name: linkerd-identity-issuer + namespace: linkerd + type: kubernetes.io/tls + files: + - tls.crt=issuer.crt + - tls.key=issuer.key + +# Generators for the identityTrustAnchorsPEM +generatorOptions: + disableNameSuffixHash: true + +configMapGenerator: + - name: linkerd-ca-bundle + namespace: linkerd + files: + - ca.crt + +#THIS BLOCK RESOLVES THE MERGE ERROR +# patches: +# - target: +# group: apiextensions.k8s.io +# version: v1 +# kind: CustomResourceDefinition +# patch: |- +# # This tells K8s/Kustomize to "Replace" if it sees this ID twice +# $patch: replace +# metadata: +# annotations: +# kustomize.config.k8s.io/behavior: merge + # - target: + # group: apiextensions.k8s.io + # version: v1 + # kind: CustomResourceDefinition + # name: httproutes.gateway.networking.k8s.io + # patch: |- + # apiVersion: apiextensions.k8s.io/v1 + # kind: CustomResourceDefinition + # metadata: + # name: httproutes.gateway.networking.k8s.io + # - target: + # group: apiextensions.k8s.io + # version: v1 + # kind: CustomResourceDefinition + # name: gateways.gateway.networking.k8s.io + # patch: |- + # apiVersion: apiextensions.k8s.io/v1 + # kind: CustomResourceDefinition + # metadata: + # name: gateways.gateway.networking.k8s.io + + +# If you need to override the namespace for everything at once: +# namespace: apps \ No newline at end of file diff --git a/linkerd/linkerd-control-plane.yaml b/linkerd/linkerd-control-plane.yaml new file mode 100644 index 0000000000000000000000000000000000000000..03e9d0435e33b492a6f41582ab6dbb74124fb834 GIT binary patch literal 155136 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 literal 0 HcmV?d00001 diff --git a/linkerd/linkerd-crds.yaml b/linkerd/linkerd-crds.yaml new file mode 100644 index 0000000000000000000000000000000000000000..5dd828c509be05f8577fe75890b011e03bc473a2 GIT binary patch literal 1326920 zcmeFad6OL1b@o|*J7T^=3$n~bZnQ{9l;u�f8IBT<|s^zB4u{VFzhpn8!;`o-aNUV-zuJ7( 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

-C|jtrR@?NBt3|q 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 z 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 CP25rd literal 0 HcmV?d00001 diff --git a/linkerd/linkerd-viz.yaml b/linkerd/linkerd-viz.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a74fd3cd0ee53ee9af83cf5273d1bfe2f2d41ea3 GIT binary patch literal 106058 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 NbKA`|F58z7{eP=iY&8G? literal 0 HcmV?d00001 diff --git a/linkerd/values-identity.yaml b/linkerd/values-identity.yaml new file mode 100644 index 0000000..cfa1374 --- /dev/null +++ b/linkerd/values-identity.yaml @@ -0,0 +1,55 @@ +nodeSelector: + kubernetes.io/os: linux + # Add a specific worker label if your cluster uses them + # node-role.kubernetes.io/worker: "" + +# Ensure we DON'T tolerate the master node taints +tolerations: [] + +# Force Linkerd to spread across different worker nodes for HA +# affinity: +# nodeAffinity: +# requiredDuringSchedulingIgnoredDuringExecution: +# nodeSelectorTerms: +# - matchExpressions: +# - key: node-role.kubernetes.io/control-plane +# operator: DoesNotExist +# - key: node-role.kubernetes.io/master +# operator: DoesNotExist + + +identityTrustAnchorsPEM: | + -----BEGIN CERTIFICATE----- + MIIBnjCCAUWgAwIBAgIUS9ANIQmpVrY6qbpdl8Arg18p9RQwCgYIKoZIzj0EAwIw + JTEjMCEGA1UEAwwacm9vdC5saW5rZXJkLmNsdXN0ZXIubG9jYWwwHhcNMjYwMzEz + MjMwNDA3WhcNMzYwMzEwMjMwNDA3WjAlMSMwIQYDVQQDDBpyb290LmxpbmtlcmQu + Y2x1c3Rlci5sb2NhbDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCnfBtfAlgkD + LVSDBOuJWBv9D0kRT2p5uZMUGY24KdR9KSua7/sRGkdnsm8NBOjj1a41wSm8N8W9 + NLV2QlPzMdmjUzBRMB0GA1UdDgQWBBTW7CLrdube2U7hqMLBkLaVTJ+f4TAfBgNV + HSMEGDAWgBTW7CLrdube2U7hqMLBkLaVTJ+f4TAPBgNVHRMBAf8EBTADAQH/MAoG + CCqGSM49BAMCA0cAMEQCICMroSX4r66e28v/rtycwBX4bvwzTe7QDuszGmkcIcEs + AiABAkzRzbYpipt+fuGliHDYMsEZd1zPqTuLCK3xp2jjHg== + -----END CERTIFICATE----- + +identity: + issuer: + tls: + crtPEM: | + -----BEGIN CERTIFICATE----- + MIIBtjCCAVygAwIBAgIUdd/8fSGsrGBI7FrbZ1lA+jS489UwCgYIKoZIzj0EAwIw + JTEjMCEGA1UEAwwacm9vdC5saW5rZXJkLmNsdXN0ZXIubG9jYWwwHhcNMjYwMzEz + MjMwNTI2WhcNMjcwMzEzMjMwNTI2WjApMScwJQYDVQQDDB5pZGVudGl0eS5saW5r + ZXJkLmNsdXN0ZXIubG9jYWwwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQHU88u + R1qhnrxcDySFKz4BqLYs/fjGwvD77PqxLp37K23lzbM5UpXVB06cnCB03plrbxg3 + VzM4Y3CTU/OgCN0/o2YwZDASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQE + AwIBhjAdBgNVHQ4EFgQUc3Y9kn7kJpEqFvXxFtE302o289AwHwYDVR0jBBgwFoAU + 1uwi63bm3tlO4ajCwZC2lUyfn+EwCgYIKoZIzj0EAwIDSAAwRQIhAPjSBYscyTfN + 99dm/or5OxYSPIS6WbaySvHrfar+9ReCAiAMkdZ8P5IrGUERW2kKhzU6jWsPfFbw + ddI8y/PwVTDuRw== + -----END CERTIFICATE----- + keyPEM: | + -----BEGIN EC PRIVATE KEY----- + MHcCAQEEIHxdoj94UpVxEjt+kNoLePmjiyRANZFpMVqQj2wAQdeyoAoGCCqGSM49 + AwEHoUQDQgAEB1PPLkdaoZ68XA8khSs+Aai2LP34xsLw++z6sS6d+ytt5c2zOVKV + 1QdOnJwgdN6Za28YN1czOGNwk1PzoAjdPw== + -----END EC PRIVATE KEY----- diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/.helmignore b/longhorn-system/charts/longhorn-v1.5.3/longhorn/.helmignore new file mode 100644 index 0000000..f0c1319 --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/Chart.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/Chart.yaml new file mode 100644 index 0000000..7767568 --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/Chart.yaml @@ -0,0 +1,28 @@ +apiVersion: v1 +appVersion: v1.5.3 +description: Longhorn is a distributed block storage system for Kubernetes. +home: https://github.com/longhorn/longhorn +icon: https://raw.githubusercontent.com/cncf/artwork/master/projects/longhorn/icon/color/longhorn-icon-color.png +keywords: +- longhorn +- storage +- distributed +- block +- device +- iscsi +- nfs +kubeVersion: '>=1.21.0-0' +maintainers: +- email: maintainers@longhorn.io + name: Longhorn maintainers +name: longhorn +sources: +- https://github.com/longhorn/longhorn +- https://github.com/longhorn/longhorn-engine +- https://github.com/longhorn/longhorn-instance-manager +- https://github.com/longhorn/longhorn-share-manager +- https://github.com/longhorn/longhorn-manager +- https://github.com/longhorn/longhorn-ui +- https://github.com/longhorn/longhorn-tests +- https://github.com/longhorn/backing-image-manager +version: 1.5.3 diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/README.md b/longhorn-system/charts/longhorn-v1.5.3/longhorn/README.md new file mode 100644 index 0000000..012c058 --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/README.md @@ -0,0 +1,78 @@ +# Longhorn Chart + +> **Important**: Please install the Longhorn chart in the `longhorn-system` namespace only. + +> **Warning**: Longhorn doesn't support downgrading from a higher version to a lower version. + +## Source Code + +Longhorn is 100% open source software. Project source code is spread across a number of repos: + +1. Longhorn Engine -- Core controller/replica logic https://github.com/longhorn/longhorn-engine +2. Longhorn Instance Manager -- Controller/replica instance lifecycle management https://github.com/longhorn/longhorn-instance-manager +3. Longhorn Share Manager -- NFS provisioner that exposes Longhorn volumes as ReadWriteMany volumes. https://github.com/longhorn/longhorn-share-manager +4. Backing Image Manager -- Backing image file lifecycle management. https://github.com/longhorn/backing-image-manager +5. Longhorn Manager -- Longhorn orchestration, includes CSI driver for Kubernetes https://github.com/longhorn/longhorn-manager +6. Longhorn UI -- Dashboard https://github.com/longhorn/longhorn-ui + +## Prerequisites + +1. A container runtime compatible with Kubernetes (Docker v1.13+, containerd v1.3.7+, etc.) +2. Kubernetes >= v1.21 +3. Make sure `bash`, `curl`, `findmnt`, `grep`, `awk` and `blkid` has been installed in all nodes of the Kubernetes cluster. +4. Make sure `open-iscsi` has been installed, and the `iscsid` daemon is running on all nodes of the Kubernetes cluster. For GKE, recommended Ubuntu as guest OS image since it contains `open-iscsi` already. + +## Upgrading to Kubernetes v1.25+ + +Starting in Kubernetes v1.25, [Pod Security Policies](https://kubernetes.io/docs/concepts/security/pod-security-policy/) have been removed from the Kubernetes API. + +As a result, **before upgrading to Kubernetes v1.25** (or on a fresh install in a Kubernetes v1.25+ cluster), users are expected to perform an in-place upgrade of this chart with `enablePSP` set to `false` if it has been previously set to `true`. + +> **Note:** +> If you upgrade your cluster to Kubernetes v1.25+ before removing PSPs via a `helm upgrade` (even if you manually clean up resources), **it will leave the Helm release in a broken state within the cluster such that further Helm operations will not work (`helm uninstall`, `helm upgrade`, etc.).** +> +> If your charts get stuck in this state, you may have to clean up your Helm release secrets. +Upon setting `enablePSP` to false, the chart will remove any PSP resources deployed on its behalf from the cluster. This is the default setting for this chart. + +As a replacement for PSPs, [Pod Security Admission](https://kubernetes.io/docs/concepts/security/pod-security-admission/) should be used. Please consult the Longhorn docs for more details on how to configure your chart release namespaces to work with the new Pod Security Admission and apply Pod Security Standards. + +## Installation +1. Add Longhorn chart repository. +``` +helm repo add longhorn https://charts.longhorn.io +``` + +2. Update local Longhorn chart information from chart repository. +``` +helm repo update +``` + +3. Install Longhorn chart. +- With Helm 2, the following command will create the `longhorn-system` namespace and install the Longhorn chart together. +``` +helm install longhorn/longhorn --name longhorn --namespace longhorn-system +``` +- With Helm 3, the following commands will create the `longhorn-system` namespace first, then install the Longhorn chart. + +``` +kubectl create namespace longhorn-system +helm install longhorn longhorn/longhorn --namespace longhorn-system +``` + +## Uninstallation + +With Helm 2 to uninstall Longhorn. +``` +kubectl -n longhorn-system patch -p '{"value": "true"}' --type=merge lhs deleting-confirmation-flag +helm delete longhorn --purge +``` + +With Helm 3 to uninstall Longhorn. +``` +kubectl -n longhorn-system patch -p '{"value": "true"}' --type=merge lhs deleting-confirmation-flag +helm uninstall longhorn -n longhorn-system +kubectl delete namespace longhorn-system +``` + +--- +Please see [link](https://github.com/longhorn/longhorn) for more information. diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/app-readme.md b/longhorn-system/charts/longhorn-v1.5.3/longhorn/app-readme.md new file mode 100644 index 0000000..cb23135 --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/app-readme.md @@ -0,0 +1,11 @@ +# Longhorn + +Longhorn is a lightweight, reliable and easy to use distributed block storage system for Kubernetes. Once deployed, users can leverage persistent volumes provided by Longhorn. + +Longhorn creates a dedicated storage controller for each volume and synchronously replicates the volume across multiple replicas stored on multiple nodes. The storage controller and replicas are themselves orchestrated using Kubernetes. Longhorn supports snapshots, backups and even allows you to schedule recurring snapshots and backups! + +**Important**: Please install Longhorn chart in `longhorn-system` namespace only. + +**Warning**: Longhorn doesn't support downgrading from a higher version to a lower version. + +[Chart Documentation](https://github.com/longhorn/longhorn/blob/master/chart/README.md) diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/questions.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/questions.yaml new file mode 100644 index 0000000..e532e0c --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/questions.yaml @@ -0,0 +1,890 @@ +categories: +- storage +namespace: longhorn-system +questions: +- variable: image.defaultImage + default: "true" + description: "Use default Longhorn images" + label: Use Default Images + type: boolean + show_subquestion_if: false + group: "Longhorn Images" + subquestions: + - variable: image.longhorn.manager.repository + default: longhornio/longhorn-manager + description: "Specify Longhorn Manager Image Repository" + type: string + label: Longhorn Manager Image Repository + group: "Longhorn Images Settings" + - variable: image.longhorn.manager.tag + default: v1.5.3 + description: "Specify Longhorn Manager Image Tag" + type: string + label: Longhorn Manager Image Tag + group: "Longhorn Images Settings" + - variable: image.longhorn.engine.repository + default: longhornio/longhorn-engine + description: "Specify Longhorn Engine Image Repository" + type: string + label: Longhorn Engine Image Repository + group: "Longhorn Images Settings" + - variable: image.longhorn.engine.tag + default: v1.5.3 + description: "Specify Longhorn Engine Image Tag" + type: string + label: Longhorn Engine Image Tag + group: "Longhorn Images Settings" + - variable: image.longhorn.ui.repository + default: longhornio/longhorn-ui + description: "Specify Longhorn UI Image Repository" + type: string + label: Longhorn UI Image Repository + group: "Longhorn Images Settings" + - variable: image.longhorn.ui.tag + default: v1.5.3 + description: "Specify Longhorn UI Image Tag" + type: string + label: Longhorn UI Image Tag + group: "Longhorn Images Settings" + - variable: image.longhorn.instanceManager.repository + default: longhornio/longhorn-instance-manager + description: "Specify Longhorn Instance Manager Image Repository" + type: string + label: Longhorn Instance Manager Image Repository + group: "Longhorn Images Settings" + - variable: image.longhorn.instanceManager.tag + default: v1.5.3 + description: "Specify Longhorn Instance Manager Image Tag" + type: string + label: Longhorn Instance Manager Image Tag + group: "Longhorn Images Settings" + - variable: image.longhorn.shareManager.repository + default: longhornio/longhorn-share-manager + description: "Specify Longhorn Share Manager Image Repository" + type: string + label: Longhorn Share Manager Image Repository + group: "Longhorn Images Settings" + - variable: image.longhorn.shareManager.tag + default: v1.5.3 + description: "Specify Longhorn Share Manager Image Tag" + type: string + label: Longhorn Share Manager Image Tag + group: "Longhorn Images Settings" + - variable: image.longhorn.backingImageManager.repository + default: longhornio/backing-image-manager + description: "Specify Longhorn Backing Image Manager Image Repository" + type: string + label: Longhorn Backing Image Manager Image Repository + group: "Longhorn Images Settings" + - variable: image.longhorn.backingImageManager.tag + default: v1.5.3 + description: "Specify Longhorn Backing Image Manager Image Tag" + type: string + label: Longhorn Backing Image Manager Image Tag + group: "Longhorn Images Settings" + - variable: image.longhorn.supportBundleKit.repository + default: longhornio/support-bundle-kit + description: "Specify Longhorn Support Bundle Manager Image Repository" + type: string + label: Longhorn Support Bundle Kit Image Repository + group: "Longhorn Images Settings" + - variable: image.longhorn.supportBundleKit.tag + default: v0.0.27 + description: "Specify Longhorn Support Bundle Manager Image Tag" + type: string + label: Longhorn Support Bundle Kit Image Tag + group: "Longhorn Images Settings" + - variable: image.csi.attacher.repository + default: longhornio/csi-attacher + description: "Specify CSI attacher image repository. Leave blank to autodetect." + type: string + label: Longhorn CSI Attacher Image Repository + group: "Longhorn CSI Driver Images" + - variable: image.csi.attacher.tag + default: v4.2.0 + description: "Specify CSI attacher image tag. Leave blank to autodetect." + type: string + label: Longhorn CSI Attacher Image Tag + group: "Longhorn CSI Driver Images" + - variable: image.csi.provisioner.repository + default: longhornio/csi-provisioner + description: "Specify CSI provisioner image repository. Leave blank to autodetect." + type: string + label: Longhorn CSI Provisioner Image Repository + group: "Longhorn CSI Driver Images" + - variable: image.csi.provisioner.tag + default: v3.4.1 + description: "Specify CSI provisioner image tag. Leave blank to autodetect." + type: string + label: Longhorn CSI Provisioner Image Tag + group: "Longhorn CSI Driver Images" + - variable: image.csi.nodeDriverRegistrar.repository + default: longhornio/csi-node-driver-registrar + description: "Specify CSI Node Driver Registrar image repository. Leave blank to autodetect." + type: string + label: Longhorn CSI Node Driver Registrar Image Repository + group: "Longhorn CSI Driver Images" + - variable: image.csi.nodeDriverRegistrar.tag + default: v2.7.0 + description: "Specify CSI Node Driver Registrar image tag. Leave blank to autodetect." + type: string + label: Longhorn CSI Node Driver Registrar Image Tag + group: "Longhorn CSI Driver Images" + - variable: image.csi.resizer.repository + default: longhornio/csi-resizer + description: "Specify CSI Driver Resizer image repository. Leave blank to autodetect." + type: string + label: Longhorn CSI Driver Resizer Image Repository + group: "Longhorn CSI Driver Images" + - variable: image.csi.resizer.tag + default: v1.7.0 + description: "Specify CSI Driver Resizer image tag. Leave blank to autodetect." + type: string + label: Longhorn CSI Driver Resizer Image Tag + group: "Longhorn CSI Driver Images" + - variable: image.csi.snapshotter.repository + default: longhornio/csi-snapshotter + description: "Specify CSI Driver Snapshotter image repository. Leave blank to autodetect." + type: string + label: Longhorn CSI Driver Snapshotter Image Repository + group: "Longhorn CSI Driver Images" + - variable: image.csi.snapshotter.tag + default: v6.2.1 + description: "Specify CSI Driver Snapshotter image tag. Leave blank to autodetect." + type: string + label: Longhorn CSI Driver Snapshotter Image Tag + group: "Longhorn CSI Driver Images" + - variable: image.csi.livenessProbe.repository + default: longhornio/livenessprobe + description: "Specify CSI liveness probe image repository. Leave blank to autodetect." + type: string + label: Longhorn CSI Liveness Probe Image Repository + group: "Longhorn CSI Driver Images" + - variable: image.csi.livenessProbe.tag + default: v2.9.0 + description: "Specify CSI liveness probe image tag. Leave blank to autodetect." + type: string + label: Longhorn CSI Liveness Probe Image Tag + group: "Longhorn CSI Driver Images" +- variable: privateRegistry.registryUrl + label: Private registry URL + description: "URL of private registry. Leave blank to apply system default registry." + group: "Private Registry Settings" + type: string + default: "" +- variable: privateRegistry.registrySecret + label: Private registry secret name + description: "If create a new private registry secret is true, create a Kubernetes secret with this name; else use the existing secret of this name. Use it to pull images from your private registry." + group: "Private Registry Settings" + type: string + default: "" +- variable: privateRegistry.createSecret + default: "true" + description: "Create a new private registry secret" + type: boolean + group: "Private Registry Settings" + label: Create Secret for Private Registry Settings + show_subquestion_if: true + subquestions: + - variable: privateRegistry.registryUser + label: Private registry user + description: "User used to authenticate to private registry." + type: string + default: "" + - variable: privateRegistry.registryPasswd + label: Private registry password + description: "Password used to authenticate to private registry." + type: password + default: "" +- variable: longhorn.default_setting + default: "false" + description: "Customize the default settings before installing Longhorn for the first time. This option will only work if the cluster hasn't installed Longhorn." + label: "Customize Default Settings" + type: boolean + show_subquestion_if: true + group: "Longhorn Default Settings" + subquestions: + - variable: csi.kubeletRootDir + default: + description: "Specify kubelet root-dir. Leave blank to autodetect." + type: string + label: Kubelet Root Directory + group: "Longhorn CSI Driver Settings" + - variable: csi.attacherReplicaCount + type: int + default: 3 + min: 1 + max: 10 + description: "Specify replica count of CSI Attacher. By default 3." + label: Longhorn CSI Attacher replica count + group: "Longhorn CSI Driver Settings" + - variable: csi.provisionerReplicaCount + type: int + default: 3 + min: 1 + max: 10 + description: "Specify replica count of CSI Provisioner. By default 3." + label: Longhorn CSI Provisioner replica count + group: "Longhorn CSI Driver Settings" + - variable: csi.resizerReplicaCount + type: int + default: 3 + min: 1 + max: 10 + description: "Specify replica count of CSI Resizer. By default 3." + label: Longhorn CSI Resizer replica count + group: "Longhorn CSI Driver Settings" + - variable: csi.snapshotterReplicaCount + type: int + default: 3 + min: 1 + max: 10 + description: "Specify replica count of CSI Snapshotter. By default 3." + label: Longhorn CSI Snapshotter replica count + group: "Longhorn CSI Driver Settings" + - variable: defaultSettings.backupTarget + label: Backup Target + description: "The endpoint used to access the backupstore. NFS and S3 are supported." + group: "Longhorn Default Settings" + type: string + default: + - variable: defaultSettings.backupTargetCredentialSecret + label: Backup Target Credential Secret + description: "The name of the Kubernetes secret associated with the backup target." + group: "Longhorn Default Settings" + type: string + default: + - variable: defaultSettings.allowRecurringJobWhileVolumeDetached + label: Allow Recurring Job While Volume Is Detached + description: 'If this setting is enabled, Longhorn will automatically attaches the volume and takes snapshot/backup when it is the time to do recurring snapshot/backup. +Note that the volume is not ready for workload during the period when the volume was automatically attached. Workload will have to wait until the recurring job finishes.' + group: "Longhorn Default Settings" + type: boolean + default: "false" + - variable: defaultSettings.createDefaultDiskLabeledNodes + label: Create Default Disk on Labeled Nodes + description: 'Create default Disk automatically only on Nodes with the label "node.longhorn.io/create-default-disk=true" if no other disks exist. If disabled, the default disk will be created on all new nodes when each node is first added.' + group: "Longhorn Default Settings" + type: boolean + default: "false" + - variable: defaultSettings.defaultDataPath + label: Default Data Path + description: 'Default path to use for storing data on a host. By default "/var/lib/longhorn/"' + group: "Longhorn Default Settings" + type: string + default: "/var/lib/longhorn/" + - variable: defaultSettings.defaultDataLocality + label: Default Data Locality + description: 'We say a Longhorn volume has data locality if there is a local replica of the volume on the same node as the pod which is using the volume. +This setting specifies the default data locality when a volume is created from the Longhorn UI. For Kubernetes configuration, update the `dataLocality` in the StorageClass +The available modes are: +- **disabled**. This is the default option. There may or may not be a replica on the same node as the attached volume (workload) +- **best-effort**. This option instructs Longhorn to try to keep a replica on the same node as the attached volume (workload). Longhorn will not stop the volume, even if it cannot keep a replica local to the attached volume (workload) due to environment limitation, e.g. not enough disk space, incompatible disk tags, etc.' + group: "Longhorn Default Settings" + type: enum + options: + - "disabled" + - "best-effort" + default: "disabled" + - variable: defaultSettings.replicaSoftAntiAffinity + label: Replica Node Level Soft Anti-Affinity + description: 'Allow scheduling on nodes with existing healthy replicas of the same volume. By default false.' + group: "Longhorn Default Settings" + type: boolean + default: "false" + - variable: defaultSettings.replicaAutoBalance + label: Replica Auto Balance + description: 'Enable this setting automatically rebalances replicas when discovered an available node. +The available global options are: +- **disabled**. This is the default option. No replica auto-balance will be done. +- **least-effort**. This option instructs Longhorn to balance replicas for minimal redundancy. +- **best-effort**. This option instructs Longhorn to balance replicas for even redundancy. +Longhorn also support individual volume setting. The setting can be specified in volume.spec.replicaAutoBalance, this overrules the global setting. +The available volume spec options are: +- **ignored**. This is the default option that instructs Longhorn to inherit from the global setting. +- **disabled**. This option instructs Longhorn no replica auto-balance should be done. +- **least-effort**. This option instructs Longhorn to balance replicas for minimal redundancy. +- **best-effort**. This option instructs Longhorn to balance replicas for even redundancy.' + group: "Longhorn Default Settings" + type: enum + options: + - "disabled" + - "least-effort" + - "best-effort" + default: "disabled" + - variable: defaultSettings.storageOverProvisioningPercentage + label: Storage Over Provisioning Percentage + description: "The over-provisioning percentage defines how much storage can be allocated relative to the hard drive's capacity. By default 200." + group: "Longhorn Default Settings" + type: int + min: 0 + default: 200 + - variable: defaultSettings.storageMinimalAvailablePercentage + label: Storage Minimal Available Percentage + description: "If the minimum available disk capacity exceeds the actual percentage of available disk capacity, the disk becomes unschedulable until more space is freed up. By default 25." + group: "Longhorn Default Settings" + type: int + min: 0 + max: 100 + default: 25 + - variable: defaultSettings.storageReservedPercentageForDefaultDisk + label: Storage Reserved Percentage For Default Disk + description: "The reserved percentage specifies the percentage of disk space that will not be allocated to the default disk on each new Longhorn node." + group: "Longhorn Default Settings" + type: int + min: 0 + max: 100 + default: 30 + - variable: defaultSettings.upgradeChecker + label: Enable Upgrade Checker + description: 'Upgrade Checker will check for new Longhorn version periodically. When there is a new version available, a notification will appear in the UI. By default true.' + group: "Longhorn Default Settings" + type: boolean + default: "true" + - variable: defaultSettings.defaultReplicaCount + label: Default Replica Count + description: "The default number of replicas when a volume is created from the Longhorn UI. For Kubernetes configuration, update the `numberOfReplicas` in the StorageClass. By default 3." + group: "Longhorn Default Settings" + type: int + min: 1 + max: 20 + default: 3 + - variable: defaultSettings.defaultLonghornStaticStorageClass + label: Default Longhorn Static StorageClass Name + description: "The 'storageClassName' is given to PVs and PVCs that are created for an existing Longhorn volume. The StorageClass name can also be used as a label, so it is possible to use a Longhorn StorageClass to bind a workload to an existing PV without creating a Kubernetes StorageClass object. By default 'longhorn-static'." + group: "Longhorn Default Settings" + type: string + default: "longhorn-static" + - variable: defaultSettings.backupstorePollInterval + label: Backupstore Poll Interval + description: "In seconds. The backupstore poll interval determines how often Longhorn checks the backupstore for new backups. Set to 0 to disable the polling. By default 300." + group: "Longhorn Default Settings" + type: int + min: 0 + default: 300 + - variable: defaultSettings.failedBackupTTL + label: Failed Backup Time to Live + description: "In minutes. This setting determines how long Longhorn will keep the backup resource that was failed. Set to 0 to disable the auto-deletion. +Failed backups will be checked and cleaned up during backupstore polling which is controlled by **Backupstore Poll Interval** setting. +Hence this value determines the minimal wait interval of the cleanup. And the actual cleanup interval is multiple of **Backupstore Poll Interval**. +Disabling **Backupstore Poll Interval** also means to disable failed backup auto-deletion." + group: "Longhorn Default Settings" + type: int + min: 0 + default: 1440 + - variable: defaultSettings.restoreVolumeRecurringJobs + label: Restore Volume Recurring Jobs + description: "Restore recurring jobs from the backup volume on the backup target and create recurring jobs if not exist during a backup restoration. +Longhorn also supports individual volume setting. The setting can be specified on Backup page when making a backup restoration, this overrules the global setting. +The available volume setting options are: +- **ignored**. This is the default option that instructs Longhorn to inherit from the global setting. +- **enabled**. This option instructs Longhorn to restore recurring jobs/groups from the backup target forcibly. +- **disabled**. This option instructs Longhorn no restoring recurring jobs/groups should be done." + group: "Longhorn Default Settings" + type: boolean + default: "false" + - variable: defaultSettings.recurringSuccessfulJobsHistoryLimit + label: Cronjob Successful Jobs History Limit + description: "This setting specifies how many successful backup or snapshot job histories should be retained. History will not be retained if the value is 0." + group: "Longhorn Default Settings" + type: int + min: 0 + default: 1 + - variable: defaultSettings.recurringFailedJobsHistoryLimit + label: Cronjob Failed Jobs History Limit + description: "This setting specifies how many failed backup or snapshot job histories should be retained. History will not be retained if the value is 0." + group: "Longhorn Default Settings" + type: int + min: 0 + default: 1 + - variable: defaultSettings.supportBundleFailedHistoryLimit + label: SupportBundle Failed History Limit + description: "This setting specifies how many failed support bundles can exist in the cluster. +The retained failed support bundle is for analysis purposes and needs to clean up manually. +Set this value to **0** to have Longhorn automatically purge all failed support bundles." + group: "Longhorn Default Settings" + type: int + min: 0 + default: 1 + - variable: defaultSettings.autoSalvage + label: Automatic salvage + description: "If enabled, volumes will be automatically salvaged when all the replicas become faulty e.g. due to network disconnection. Longhorn will try to figure out which replica(s) are usable, then use them for the volume. By default true." + group: "Longhorn Default Settings" + type: boolean + default: "true" + - variable: defaultSettings.autoDeletePodWhenVolumeDetachedUnexpectedly + label: Automatically Delete Workload Pod when The Volume Is Detached Unexpectedly + description: 'If enabled, Longhorn will automatically delete the workload pod that is managed by a controller (e.g. deployment, statefulset, daemonset, etc...) when Longhorn volume is detached unexpectedly (e.g. during Kubernetes upgrade, Docker reboot, or network disconnect). By deleting the pod, its controller restarts the pod and Kubernetes handles volume reattachment and remount. +If disabled, Longhorn will not delete the workload pod that is managed by a controller. You will have to manually restart the pod to reattach and remount the volume. +**Note:** This setting does not apply to the workload pods that do not have a controller. Longhorn never deletes them.' + group: "Longhorn Default Settings" + type: boolean + default: "true" + - variable: defaultSettings.disableSchedulingOnCordonedNode + label: Disable Scheduling On Cordoned Node + description: "Disable Longhorn manager to schedule replica on Kubernetes cordoned node. By default true." + group: "Longhorn Default Settings" + type: boolean + default: "true" + - variable: defaultSettings.replicaZoneSoftAntiAffinity + label: Replica Zone Level Soft Anti-Affinity + description: "Allow scheduling new Replicas of Volume to the Nodes in the same Zone as existing healthy Replicas. Nodes don't belong to any Zone will be treated as in the same Zone. Notice that Longhorn relies on label `topology.kubernetes.io/zone=` in the Kubernetes node object to identify the zone. By default true." + group: "Longhorn Default Settings" + type: boolean + default: "true" + - variable: defaultSettings.nodeDownPodDeletionPolicy + label: Pod Deletion Policy When Node is Down + description: "Defines the Longhorn action when a Volume is stuck with a StatefulSet/Deployment Pod on a node that is down. +- **do-nothing** is the default Kubernetes behavior of never force deleting StatefulSet/Deployment terminating pods. Since the pod on the node that is down isn't removed, Longhorn volumes are stuck on nodes that are down. +- **delete-statefulset-pod** Longhorn will force delete StatefulSet terminating pods on nodes that are down to release Longhorn volumes so that Kubernetes can spin up replacement pods. +- **delete-deployment-pod** Longhorn will force delete Deployment terminating pods on nodes that are down to release Longhorn volumes so that Kubernetes can spin up replacement pods. +- **delete-both-statefulset-and-deployment-pod** Longhorn will force delete StatefulSet/Deployment terminating pods on nodes that are down to release Longhorn volumes so that Kubernetes can spin up replacement pods." + group: "Longhorn Default Settings" + type: enum + options: + - "do-nothing" + - "delete-statefulset-pod" + - "delete-deployment-pod" + - "delete-both-statefulset-and-deployment-pod" + default: "do-nothing" + - variable: defaultSettings.nodeDrainPolicy + label: Node Drain Policy + description: "Define the policy to use when a node with the last healthy replica of a volume is drained. +- **block-if-contains-last-replica** Longhorn will block the drain when the node contains the last healthy replica of a volume. +- **allow-if-replica-is-stopped** Longhorn will allow the drain when the node contains the last healthy replica of a volume but the replica is stopped. WARNING: possible data loss if the node is removed after draining. Select this option if you want to drain the node and do in-place upgrade/maintenance. +- **always-allow** Longhorn will allow the drain even though the node contains the last healthy replica of a volume. WARNING: possible data loss if the node is removed after draining. Also possible data corruption if the last replica was running during the draining." + group: "Longhorn Default Settings" + type: enum + options: + - "block-if-contains-last-replica" + - "allow-if-replica-is-stopped" + - "always-allow" + default: "block-if-contains-last-replica" + - variable: defaultSettings.replicaReplenishmentWaitInterval + label: Replica Replenishment Wait Interval + description: "In seconds. The interval determines how long Longhorn will wait at least in order to reuse the existing data on a failed replica rather than directly creating a new replica for a degraded volume. +Warning: This option works only when there is a failed replica in the volume. And this option may block the rebuilding for a while in the case." + group: "Longhorn Default Settings" + type: int + min: 0 + default: 600 + - variable: defaultSettings.concurrentReplicaRebuildPerNodeLimit + label: Concurrent Replica Rebuild Per Node Limit + description: "This setting controls how many replicas on a node can be rebuilt simultaneously. +Typically, Longhorn can block the replica starting once the current rebuilding count on a node exceeds the limit. But when the value is 0, it means disabling the replica rebuilding. +WARNING: +- The old setting \"Disable Replica Rebuild\" is replaced by this setting. +- Different from relying on replica starting delay to limit the concurrent rebuilding, if the rebuilding is disabled, replica object replenishment will be directly skipped. +- When the value is 0, the eviction and data locality feature won't work. But this shouldn't have any impact to any current replica rebuild and backup restore." + group: "Longhorn Default Settings" + type: int + min: 0 + default: 5 + - variable: defaultSettings.concurrentVolumeBackupRestorePerNodeLimit + label: Concurrent Volume Backup Restore Per Node Limit + description: "This setting controls how many volumes on a node can restore the backup concurrently. +Longhorn blocks the backup restore once the restoring volume count exceeds the limit. +Set the value to **0** to disable backup restore." + group: "Longhorn Default Settings" + type: int + min: 0 + default: 5 + - variable: defaultSettings.disableRevisionCounter + label: Disable Revision Counter + description: "This setting is only for volumes created by UI. By default, this is false meaning there will be a reivision counter file to track every write to the volume. During salvage recovering Longhorn will pick the replica with largest reivision counter as candidate to recover the whole volume. If revision counter is disabled, Longhorn will not track every write to the volume. During the salvage recovering, Longhorn will use the 'volume-head-xxx.img' file last modification time and file size to pick the replica candidate to recover the whole volume." + group: "Longhorn Default Settings" + type: boolean + default: "false" + - variable: defaultSettings.systemManagedPodsImagePullPolicy + label: System Managed Pod Image Pull Policy + description: "This setting defines the Image Pull Policy of Longhorn system managed pods, e.g. instance manager, engine image, CSI driver, etc. The new Image Pull Policy will only apply after the system managed pods restart." + group: "Longhorn Default Settings" + type: enum + options: + - "if-not-present" + - "always" + - "never" + default: "if-not-present" + - variable: defaultSettings.allowVolumeCreationWithDegradedAvailability + label: Allow Volume Creation with Degraded Availability + description: "This setting allows user to create and attach a volume that doesn't have all the replicas scheduled at the time of creation." + group: "Longhorn Default Settings" + type: boolean + default: "true" + - variable: defaultSettings.autoCleanupSystemGeneratedSnapshot + label: Automatically Cleanup System Generated Snapshot + description: "This setting enables Longhorn to automatically cleanup the system generated snapshot after replica rebuild is done." + group: "Longhorn Default Settings" + type: boolean + default: "true" + - variable: defaultSettings.concurrentAutomaticEngineUpgradePerNodeLimit + label: Concurrent Automatic Engine Upgrade Per Node Limit + description: "This setting controls how Longhorn automatically upgrades volumes' engines to the new default engine image after upgrading Longhorn manager. The value of this setting specifies the maximum number of engines per node that are allowed to upgrade to the default engine image at the same time. If the value is 0, Longhorn will not automatically upgrade volumes' engines to default version." + group: "Longhorn Default Settings" + type: int + min: 0 + default: 0 + - variable: defaultSettings.backingImageCleanupWaitInterval + label: Backing Image Cleanup Wait Interval + description: "This interval in minutes determines how long Longhorn will wait before cleaning up the backing image file when there is no replica in the disk using it." + group: "Longhorn Default Settings" + type: int + min: 0 + default: 60 + - variable: defaultSettings.backingImageRecoveryWaitInterval + label: Backing Image Recovery Wait Interval + description: "This interval in seconds determines how long Longhorn will wait before re-downloading the backing image file when all disk files of this backing image become failed or unknown. + WARNING: + - This recovery only works for the backing image of which the creation type is \"download\". + - File state \"unknown\" means the related manager pods on the pod is not running or the node itself is down/disconnected." + group: "Longhorn Default Settings" + type: int + min: 0 + default: 300 + - variable: defaultSettings.guaranteedInstanceManagerCPU + label: Guaranteed Instance Manager CPU + description: "This integer value indicates how many percentage of the total allocatable CPU on each node will be reserved for each instance manager Pod. For example, 10 means 10% of the total CPU on a node will be allocated to each instance manager pod on this node. This will help maintain engine and replica stability during high node workload. + In order to prevent unexpected volume instance (engine/replica) crash as well as guarantee a relative acceptable IO performance, you can use the following formula to calculate a value for this setting: + `Guaranteed Instance Manager CPU = The estimated max Longhorn volume engine and replica count on a node * 0.1 / The total allocatable CPUs on the node * 100` + The result of above calculation doesn't mean that's the maximum CPU resources the Longhorn workloads require. To fully exploit the Longhorn volume I/O performance, you can allocate/guarantee more CPU resources via this setting. + If it's hard to estimate the usage now, you can leave it with the default value, which is 12%. Then you can tune it when there is no running workload using Longhorn volumes. + WARNING: + - Value 0 means unsetting CPU requests for instance manager pods. + - Considering the possible new instance manager pods in the further system upgrade, this integer value is range from 0 to 40. + - One more set of instance manager pods may need to be deployed when the Longhorn system is upgraded. If current available CPUs of the nodes are not enough for the new instance manager pods, you need to detach the volumes using the oldest instance manager pods so that Longhorn can clean up the old pods automatically and release the CPU resources. And the new pods with the latest instance manager image will be launched then. + - This global setting will be ignored for a node if the field \"InstanceManagerCPURequest\" on the node is set. + - After this setting is changed, all instance manager pods using this global setting on all the nodes will be automatically restarted. In other words, DO NOT CHANGE THIS SETTING WITH ATTACHED VOLUMES." + group: "Longhorn Default Settings" + type: int + min: 0 + max: 40 + default: 12 + - variable: defaultSettings.logLevel + label: Log Level + description: "The log level Panic, Fatal, Error, Warn, Info, Debug, Trace used in longhorn manager. By default Debug." + group: "Longhorn Default Settings" + type: string + default: "Info" +- variable: defaultSettings.kubernetesClusterAutoscalerEnabled + label: Kubernetes Cluster Autoscaler Enabled (Experimental) + description: "Enabling this setting will notify Longhorn that the cluster is using Kubernetes Cluster Autoscaler. + Longhorn prevents data loss by only allowing the Cluster Autoscaler to scale down a node that met all conditions: + - No volume attached to the node. + - Is not the last node containing the replica of any volume. + - Is not running backing image components pod. + - Is not running share manager components pod." + group: "Longhorn Default Settings" + type: boolean + default: false +- variable: defaultSettings.orphanAutoDeletion + label: Orphaned Data Cleanup + description: "This setting allows Longhorn to delete the orphan resource and its corresponding orphaned data automatically like stale replicas. Orphan resources on down or unknown nodes will not be cleaned up automatically." + group: "Longhorn Default Settings" + type: boolean + default: false +- variable: defaultSettings.storageNetwork + label: Storage Network + description: "Longhorn uses the storage network for in-cluster data traffic. Leave this blank to use the Kubernetes cluster network. + To segregate the storage network, input the pre-existing NetworkAttachmentDefinition in \"/\" format. + WARNING: + - The cluster must have pre-existing Multus installed, and NetworkAttachmentDefinition IPs are reachable between nodes. + - DO NOT CHANGE THIS SETTING WITH ATTACHED VOLUMES. Longhorn will try to block this setting update when there are attached volumes. + - When applying the setting, Longhorn will restart all manager, instance-manager, and backing-image-manager pods." + group: "Longhorn Default Settings" + type: string + default: +- variable: defaultSettings.deletingConfirmationFlag + label: Deleting Confirmation Flag + description: "This flag is designed to prevent Longhorn from being accidentally uninstalled which will lead to data lost. + Set this flag to **true** to allow Longhorn uninstallation. + If this flag **false**, Longhorn uninstallation job will fail. " + group: "Longhorn Default Settings" + type: boolean + default: "false" +- variable: defaultSettings.engineReplicaTimeout + label: Timeout between Engine and Replica + description: "In seconds. The setting specifies the timeout between the engine and replica(s), and the value should be between 8 to 30 seconds. The default value is 8 seconds." + group: "Longhorn Default Settings" + type: int + default: "8" +- variable: defaultSettings.snapshotDataIntegrity + label: Snapshot Data Integrity + description: "This setting allows users to enable or disable snapshot hashing and data integrity checking. + Available options are + - **disabled**: Disable snapshot disk file hashing and data integrity checking. + - **enabled**: Enables periodic snapshot disk file hashing and data integrity checking. To detect the filesystem-unaware corruption caused by bit rot or other issues in snapshot disk files, Longhorn system periodically hashes files and finds corrupted ones. Hence, the system performance will be impacted during the periodical checking. + - **fast-check**: Enable snapshot disk file hashing and fast data integrity checking. Longhorn system only hashes snapshot disk files if their are not hashed or the modification time are changed. In this mode, filesystem-unaware corruption cannot be detected, but the impact on system performance can be minimized." + group: "Longhorn Default Settings" + type: string + default: "disabled" +- variable: defaultSettings.snapshotDataIntegrityImmediateCheckAfterSnapshotCreation + label: Immediate Snapshot Data Integrity Check After Creating a Snapshot + description: "Hashing snapshot disk files impacts the performance of the system. The immediate snapshot hashing and checking can be disabled to minimize the impact after creating a snapshot." + group: "Longhorn Default Settings" + type: boolean + default: "false" +- variable: defaultSettings.snapshotDataIntegrityCronjob + label: Snapshot Data Integrity Check CronJob + description: "Unix-cron string format. The setting specifies when Longhorn checks the data integrity of snapshot disk files. + Warning: Hashing snapshot disk files impacts the performance of the system. It is recommended to run data integrity checks during off-peak times and to reduce the frequency of checks." + group: "Longhorn Default Settings" + type: string + default: "0 0 */7 * *" +- variable: defaultSettings.removeSnapshotsDuringFilesystemTrim + label: Remove Snapshots During Filesystem Trim + description: "This setting allows Longhorn filesystem trim feature to automatically mark the latest snapshot and its ancestors as removed and stops at the snapshot containing multiple children.\n\n + Since Longhorn filesystem trim feature can be applied to the volume head and the followed continuous removed or system snapshots only.\n\n + Notice that trying to trim a removed files from a valid snapshot will do nothing but the filesystem will discard this kind of in-memory trimmable file info.\n\n + Later on if you mark the snapshot as removed and want to retry the trim, you may need to unmount and remount the filesystem so that the filesystem can recollect the trimmable file info." + group: "Longhorn Default Settings" + type: boolean + default: "false" +- variable: defaultSettings.fastReplicaRebuildEnabled + label: Fast Replica Rebuild Enabled + description: "This feature supports the fast replica rebuilding. It relies on the checksum of snapshot disk files, so setting the snapshot-data-integrity to **enable** or **fast-check** is a prerequisite." + group: "Longhorn Default Settings" + type: boolean + default: false +- variable: defaultSettings.replicaFileSyncHttpClientTimeout + label: Timeout of HTTP Client to Replica File Sync Server + description: "In seconds. The setting specifies the HTTP client timeout to the file sync server." + group: "Longhorn Default Settings" + type: int + default: "30" +- variable: defaultSettings.backupCompressionMethod + label: Backup Compression Method + description: "This setting allows users to specify backup compression method. + Available options are + - **none**: Disable the compression method. Suitable for multimedia data such as encoded images and videos. + - **lz4**: Fast compression method. Suitable for flat files. + - **gzip**: A bit of higher compression ratio but relatively slow." + group: "Longhorn Default Settings" + type: string + default: "lz4" +- variable: defaultSettings.backupConcurrentLimit + label: Backup Concurrent Limit Per Backup + description: "This setting controls how many worker threads per backup concurrently." + group: "Longhorn Default Settings" + type: int + min: 1 + default: 2 +- variable: defaultSettings.restoreConcurrentLimit + label: Restore Concurrent Limit Per Backup + description: "This setting controls how many worker threads per restore concurrently." + group: "Longhorn Default Settings" + type: int + min: 1 + default: 2 +- variable: defaultSettings.v2DataEngine + label: V2 Data Engine + description: "This allows users to activate v2 data engine based on SPDK. Currently, it is in the preview phase and should not be utilized in a production environment. + WARNING: + - DO NOT CHANGE THIS SETTING WITH ATTACHED VOLUMES. Longhorn will block this setting update when there are attached volumes. + - When applying the setting, Longhorn will restart all instance-manager pods. + - When the V2 Data Engine is enabled, each instance-manager pod utilizes 1 CPU core. This high CPU usage is attributed to the spdk_tgt process running within each instance-manager pod. The spdk_tgt process is responsible for handling input/output (IO) operations and requires intensive polling. As a result, it consumes 100% of a dedicated CPU core to efficiently manage and process the IO requests, ensuring optimal performance and responsiveness for storage operations." + group: "Longhorn V2 Data Engine (Preview Feature) Settings" + type: boolean + default: false +- variable: defaultSettings.offlineReplicaRebuilding + label: Offline Replica Rebuilding + description: ""This setting allows users to enable the offline replica rebuilding for volumes using v2 data engine." + group: "Longhorn V2 Data Engine (Preview Feature) Settings" + required: true + type: enum + options: + - "enabled" + - "disabled" + default: "enabled" +- variable: persistence.defaultClass + default: "true" + description: "Set as default StorageClass for Longhorn" + label: Default Storage Class + group: "Longhorn Storage Class Settings" + required: true + type: boolean +- variable: persistence.reclaimPolicy + label: Storage Class Retain Policy + description: "Define reclaim policy (Retain or Delete)" + group: "Longhorn Storage Class Settings" + required: true + type: enum + options: + - "Delete" + - "Retain" + default: "Delete" +- variable: persistence.defaultClassReplicaCount + description: "Set replica count for Longhorn StorageClass" + label: Default Storage Class Replica Count + group: "Longhorn Storage Class Settings" + type: int + min: 1 + max: 10 + default: 3 +- variable: persistence.defaultDataLocality + description: "Set data locality for Longhorn StorageClass" + label: Default Storage Class Data Locality + group: "Longhorn Storage Class Settings" + type: enum + options: + - "disabled" + - "best-effort" + default: "disabled" +- variable: persistence.recurringJobSelector.enable + description: "Enable recurring job selector for Longhorn StorageClass" + group: "Longhorn Storage Class Settings" + label: Enable Storage Class Recurring Job Selector + type: boolean + default: false + show_subquestion_if: true + subquestions: + - variable: persistence.recurringJobSelector.jobList + description: 'Recurring job selector list for Longhorn StorageClass. Please be careful of quotes of input. e.g., [{"name":"backup", "isGroup":true}]' + label: Storage Class Recurring Job Selector List + group: "Longhorn Storage Class Settings" + type: string + default: +- variable: persistence.defaultNodeSelector.enable + description: "Enable Node selector for Longhorn StorageClass" + group: "Longhorn Storage Class Settings" + label: Enable Storage Class Node Selector + type: boolean + default: false + show_subquestion_if: true + subquestions: + - variable: persistence.defaultNodeSelector.selector + label: Storage Class Node Selector + description: 'We use NodeSelector when we want to bind PVC via StorageClass into desired mountpoint on the nodes tagged with its value' + group: "Longhorn Storage Class Settings" + type: string + default: +- variable: persistence.backingImage.enable + description: "Set backing image for Longhorn StorageClass" + group: "Longhorn Storage Class Settings" + label: Default Storage Class Backing Image + type: boolean + default: false + show_subquestion_if: true + subquestions: + - variable: persistence.backingImage.name + description: 'Specify a backing image that will be used by Longhorn volumes in Longhorn StorageClass. If not exists, the backing image data source type and backing image data source parameters should be specified so that Longhorn will create the backing image before using it.' + label: Storage Class Backing Image Name + group: "Longhorn Storage Class Settings" + type: string + default: + - variable: persistence.backingImage.expectedChecksum + description: 'Specify the expected SHA512 checksum of the selected backing image in Longhorn StorageClass. + WARNING: + - If the backing image name is not specified, setting this field is meaningless. + - It is not recommended to set this field if the data source type is \"export-from-volume\".' + label: Storage Class Backing Image Expected SHA512 Checksum + group: "Longhorn Storage Class Settings" + type: string + default: + - variable: persistence.backingImage.dataSourceType + description: 'Specify the data source type for the backing image used in Longhorn StorageClass. + If the backing image does not exists, Longhorn will use this field to create a backing image. Otherwise, Longhorn will use it to verify the selected backing image. + WARNING: + - If the backing image name is not specified, setting this field is meaningless. + - As for backing image creation with data source type \"upload\", it is recommended to do it via UI rather than StorageClass here. Uploading requires file data sending to the Longhorn backend after the object creation, which is complicated if you want to handle it manually.' + label: Storage Class Backing Image Data Source Type + group: "Longhorn Storage Class Settings" + type: enum + options: + - "" + - "download" + - "upload" + - "export-from-volume" + default: "" + - variable: persistence.backingImage.dataSourceParameters + description: "Specify the data source parameters for the backing image used in Longhorn StorageClass. + If the backing image does not exists, Longhorn will use this field to create a backing image. Otherwise, Longhorn will use it to verify the selected backing image. + This option accepts a json string of a map. e.g., '{\"url\":\"https://backing-image-example.s3-region.amazonaws.com/test-backing-image\"}'. + WARNING: + - If the backing image name is not specified, setting this field is meaningless. + - Be careful of the quotes here." + label: Storage Class Backing Image Data Source Parameters + group: "Longhorn Storage Class Settings" + type: string + default: +- variable: persistence.removeSnapshotsDuringFilesystemTrim + description: "Allow automatically removing snapshots during filesystem trim for Longhorn StorageClass" + label: Default Storage Class Remove Snapshots During Filesystem Trim + group: "Longhorn Storage Class Settings" + type: enum + options: + - "ignored" + - "enabled" + - "disabled" + default: "ignored" +- variable: ingress.enabled + default: "false" + description: "Expose app using Layer 7 Load Balancer - ingress" + type: boolean + group: "Services and Load Balancing" + label: Expose app using Layer 7 Load Balancer + show_subquestion_if: true + subquestions: + - variable: ingress.host + default: "xip.io" + description: "layer 7 Load Balancer hostname" + type: hostname + required: true + label: Layer 7 Load Balancer Hostname + - variable: ingress.path + default: "/" + description: "If ingress is enabled you can set the default ingress path" + type: string + required: true + label: Ingress Path +- variable: service.ui.type + default: "Rancher-Proxy" + description: "Define Longhorn UI service type" + type: enum + options: + - "ClusterIP" + - "NodePort" + - "LoadBalancer" + - "Rancher-Proxy" + label: Longhorn UI Service + show_if: "ingress.enabled=false" + group: "Services and Load Balancing" + show_subquestion_if: "NodePort" + subquestions: + - variable: service.ui.nodePort + default: "" + description: "NodePort port number(to set explicitly, choose port between 30000-32767)" + type: int + min: 30000 + max: 32767 + show_if: "service.ui.type=NodePort||service.ui.type=LoadBalancer" + label: UI Service NodePort number +- variable: enablePSP + default: "false" + description: "Setup a pod security policy for Longhorn workloads." + label: Pod Security Policy + type: boolean + group: "Other Settings" +- variable: global.cattle.windowsCluster.enabled + default: "false" + description: "Enable this to allow Longhorn to run on the Rancher deployed Windows cluster." + label: Rancher Windows Cluster + type: boolean + group: "Other Settings" +- variable: networkPolicies.enabled + description: "Enable NetworkPolicies to limit access to the longhorn pods. + Warning: The Rancher Proxy will not work if this feature is enabled and a custom NetworkPolicy must be added." + group: "Other Settings" + label: Network Policies + default: "false" + type: boolean + subquestions: + - variable: networkPolicies.type + label: Network Policies for Ingress + description: "Create the policy to allow access for the ingress, select the distribution." + show_if: "networkPolicies.enabled=true&&ingress.enabled=true" + type: enum + default: "rke2" + options: + - "rke1" + - "rke2" + - "k3s" diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/NOTES.txt b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/NOTES.txt new file mode 100644 index 0000000..cca7cd7 --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/NOTES.txt @@ -0,0 +1,5 @@ +Longhorn is now installed on the cluster! + +Please wait a few minutes for other Longhorn components such as CSI deployments, Engine Images, and Instance Managers to be initialized. + +Visit our documentation at https://longhorn.io/docs/ diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/_helpers.tpl b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/_helpers.tpl new file mode 100644 index 0000000..3fbc2ac --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/_helpers.tpl @@ -0,0 +1,66 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "longhorn.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +*/}} +{{- define "longhorn.fullname" -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + + +{{- define "longhorn.managerIP" -}} +{{- $fullname := (include "longhorn.fullname" .) -}} +{{- printf "http://%s-backend:9500" $fullname | trunc 63 | trimSuffix "-" -}} +{{- end -}} + + +{{- define "secret" }} +{{- printf "{\"auths\": {\"%s\": {\"auth\": \"%s\"}}}" .Values.privateRegistry.registryUrl (printf "%s:%s" .Values.privateRegistry.registryUser .Values.privateRegistry.registryPasswd | b64enc) | b64enc }} +{{- end }} + +{{- /* +longhorn.labels generates the standard Helm labels. +*/ -}} +{{- define "longhorn.labels" -}} +app.kubernetes.io/name: {{ template "longhorn.name" . }} +helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ .Chart.AppVersion }} +{{- end -}} + + +{{- define "system_default_registry" -}} +{{- if .Values.global.cattle.systemDefaultRegistry -}} +{{- printf "%s/" .Values.global.cattle.systemDefaultRegistry -}} +{{- else -}} +{{- "" -}} +{{- end -}} +{{- end -}} + +{{- define "registry_url" -}} +{{- if .Values.privateRegistry.registryUrl -}} +{{- printf "%s/" .Values.privateRegistry.registryUrl -}} +{{- else -}} +{{ include "system_default_registry" . }} +{{- end -}} +{{- end -}} + +{{- /* + define the longhorn release namespace +*/ -}} +{{- define "release_namespace" -}} +{{- if .Values.namespaceOverride -}} +{{- .Values.namespaceOverride -}} +{{- else -}} +{{- .Release.Namespace -}} +{{- end -}} +{{- end -}} diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/clusterrole.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/clusterrole.yaml new file mode 100644 index 0000000..e652a34 --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/clusterrole.yaml @@ -0,0 +1,61 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: longhorn-role + labels: {{- include "longhorn.labels" . | nindent 4 }} +rules: +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - "*" +- apiGroups: [""] + resources: ["pods", "events", "persistentvolumes", "persistentvolumeclaims","persistentvolumeclaims/status", "nodes", "proxy/nodes", "pods/log", "secrets", "services", "endpoints", "configmaps", "serviceaccounts"] + verbs: ["*"] +- apiGroups: [""] + resources: ["namespaces"] + verbs: ["get", "list"] +- apiGroups: ["apps"] + resources: ["daemonsets", "statefulsets", "deployments"] + verbs: ["*"] +- apiGroups: ["batch"] + resources: ["jobs", "cronjobs"] + verbs: ["*"] +- apiGroups: ["policy"] + resources: ["poddisruptionbudgets", "podsecuritypolicies"] + verbs: ["*"] +- apiGroups: ["scheduling.k8s.io"] + resources: ["priorityclasses"] + verbs: ["watch", "list"] +- apiGroups: ["storage.k8s.io"] + resources: ["storageclasses", "volumeattachments", "volumeattachments/status", "csinodes", "csidrivers"] + verbs: ["*"] +- apiGroups: ["snapshot.storage.k8s.io"] + resources: ["volumesnapshotclasses", "volumesnapshots", "volumesnapshotcontents", "volumesnapshotcontents/status"] + verbs: ["*"] +- apiGroups: ["longhorn.io"] + resources: ["volumes", "volumes/status", "engines", "engines/status", "replicas", "replicas/status", "settings", + "engineimages", "engineimages/status", "nodes", "nodes/status", "instancemanagers", "instancemanagers/status", + "sharemanagers", "sharemanagers/status", "backingimages", "backingimages/status", + "backingimagemanagers", "backingimagemanagers/status", "backingimagedatasources", "backingimagedatasources/status", + "backuptargets", "backuptargets/status", "backupvolumes", "backupvolumes/status", "backups", "backups/status", + "recurringjobs", "recurringjobs/status", "orphans", "orphans/status", "snapshots", "snapshots/status", + "supportbundles", "supportbundles/status", "systembackups", "systembackups/status", "systemrestores", "systemrestores/status", + "volumeattachments", "volumeattachments/status"] + verbs: ["*"] +- apiGroups: ["coordination.k8s.io"] + resources: ["leases"] + verbs: ["*"] +- apiGroups: ["metrics.k8s.io"] + resources: ["pods", "nodes"] + verbs: ["get", "list"] +- apiGroups: ["apiregistration.k8s.io"] + resources: ["apiservices"] + verbs: ["list", "watch"] +- apiGroups: ["admissionregistration.k8s.io"] + resources: ["mutatingwebhookconfigurations", "validatingwebhookconfigurations"] + verbs: ["get", "list", "create", "patch", "delete"] +- apiGroups: ["rbac.authorization.k8s.io"] + resources: ["roles", "rolebindings", "clusterrolebindings", "clusterroles"] + verbs: ["*"] diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/clusterrolebinding.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/clusterrolebinding.yaml new file mode 100644 index 0000000..8ab944b --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/clusterrolebinding.yaml @@ -0,0 +1,27 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: longhorn-bind + labels: {{- include "longhorn.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: longhorn-role +subjects: +- kind: ServiceAccount + name: longhorn-service-account + namespace: {{ include "release_namespace" . }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: longhorn-support-bundle + labels: {{- include "longhorn.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: longhorn-support-bundle + namespace: {{ include "release_namespace" . }} diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/crds.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/crds.yaml new file mode 100644 index 0000000..ac56efe --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/crds.yaml @@ -0,0 +1,3672 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + labels: {{- include "longhorn.labels" . | nindent 4 }} + longhorn-manager: "" + name: backingimagedatasources.longhorn.io +spec: + group: longhorn.io + names: + kind: BackingImageDataSource + listKind: BackingImageDataSourceList + plural: backingimagedatasources + shortNames: + - lhbids + singular: backingimagedatasource + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The current state of the pod used to provision the backing image file from source + jsonPath: .status.currentState + name: State + type: string + - description: The data source type + jsonPath: .spec.sourceType + name: SourceType + type: string + - description: The node the backing image file will be prepared on + jsonPath: .spec.nodeID + name: Node + type: string + - description: The disk the backing image file will be prepared on + jsonPath: .spec.diskUUID + name: DiskUUID + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: BackingImageDataSource is where Longhorn stores backing image data source object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - description: The system generated UUID of the provisioned backing image file + jsonPath: .spec.uuid + name: UUID + type: string + - description: The current state of the pod used to provision the backing image file from source + jsonPath: .status.currentState + name: State + type: string + - description: The data source type + jsonPath: .spec.sourceType + name: SourceType + type: string + - description: The backing image file size + jsonPath: .status.size + name: Size + type: string + - description: The node the backing image file will be prepared on + jsonPath: .spec.nodeID + name: Node + type: string + - description: The disk the backing image file will be prepared on + jsonPath: .spec.diskUUID + name: DiskUUID + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta2 + schema: + openAPIV3Schema: + description: BackingImageDataSource is where Longhorn stores backing image data source object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: BackingImageDataSourceSpec defines the desired state of the Longhorn backing image data source + properties: + checksum: + type: string + diskPath: + type: string + diskUUID: + type: string + fileTransferred: + type: boolean + nodeID: + type: string + parameters: + additionalProperties: + type: string + type: object + sourceType: + enum: + - download + - upload + - export-from-volume + type: string + uuid: + type: string + type: object + status: + description: BackingImageDataSourceStatus defines the observed state of the Longhorn backing image data source + properties: + checksum: + type: string + currentState: + type: string + ip: + type: string + message: + type: string + ownerID: + type: string + progress: + type: integer + runningParameters: + additionalProperties: + type: string + nullable: true + type: object + size: + format: int64 + type: integer + storageIP: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + labels: {{- include "longhorn.labels" . | nindent 4 }} + longhorn-manager: "" + name: backingimagemanagers.longhorn.io +spec: + group: longhorn.io + names: + kind: BackingImageManager + listKind: BackingImageManagerList + plural: backingimagemanagers + shortNames: + - lhbim + singular: backingimagemanager + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The current state of the manager + jsonPath: .status.currentState + name: State + type: string + - description: The image the manager pod will use + jsonPath: .spec.image + name: Image + type: string + - description: The node the manager is on + jsonPath: .spec.nodeID + name: Node + type: string + - description: The disk the manager is responsible for + jsonPath: .spec.diskUUID + name: DiskUUID + type: string + - description: The disk path the manager is using + jsonPath: .spec.diskPath + name: DiskPath + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: BackingImageManager is where Longhorn stores backing image manager object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - description: The current state of the manager + jsonPath: .status.currentState + name: State + type: string + - description: The image the manager pod will use + jsonPath: .spec.image + name: Image + type: string + - description: The node the manager is on + jsonPath: .spec.nodeID + name: Node + type: string + - description: The disk the manager is responsible for + jsonPath: .spec.diskUUID + name: DiskUUID + type: string + - description: The disk path the manager is using + jsonPath: .spec.diskPath + name: DiskPath + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta2 + schema: + openAPIV3Schema: + description: BackingImageManager is where Longhorn stores backing image manager object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: BackingImageManagerSpec defines the desired state of the Longhorn backing image manager + properties: + backingImages: + additionalProperties: + type: string + type: object + diskPath: + type: string + diskUUID: + type: string + image: + type: string + nodeID: + type: string + type: object + status: + description: BackingImageManagerStatus defines the observed state of the Longhorn backing image manager + properties: + apiMinVersion: + type: integer + apiVersion: + type: integer + backingImageFileMap: + additionalProperties: + properties: + currentChecksum: + type: string + message: + type: string + name: + type: string + progress: + type: integer + senderManagerAddress: + type: string + sendingReference: + type: integer + size: + format: int64 + type: integer + state: + type: string + uuid: + type: string + type: object + nullable: true + type: object + currentState: + type: string + ip: + type: string + ownerID: + type: string + storageIP: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + labels: {{- include "longhorn.labels" . | nindent 4 }} + longhorn-manager: "" + name: backingimages.longhorn.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + name: longhorn-conversion-webhook + namespace: {{ include "release_namespace" . }} + path: /v1/webhook/conversion + port: 9501 + conversionReviewVersions: + - v1beta2 + - v1beta1 + group: longhorn.io + names: + kind: BackingImage + listKind: BackingImageList + plural: backingimages + shortNames: + - lhbi + singular: backingimage + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The backing image name + jsonPath: .spec.image + name: Image + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: BackingImage is where Longhorn stores backing image object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - description: The system generated UUID + jsonPath: .status.uuid + name: UUID + type: string + - description: The source of the backing image file data + jsonPath: .spec.sourceType + name: SourceType + type: string + - description: The backing image file size in each disk + jsonPath: .status.size + name: Size + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta2 + schema: + openAPIV3Schema: + description: BackingImage is where Longhorn stores backing image object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: BackingImageSpec defines the desired state of the Longhorn backing image + properties: + checksum: + type: string + disks: + additionalProperties: + type: string + type: object + sourceParameters: + additionalProperties: + type: string + type: object + sourceType: + enum: + - download + - upload + - export-from-volume + type: string + type: object + status: + description: BackingImageStatus defines the observed state of the Longhorn backing image status + properties: + checksum: + type: string + diskFileStatusMap: + additionalProperties: + properties: + lastStateTransitionTime: + type: string + message: + type: string + progress: + type: integer + state: + type: string + type: object + nullable: true + type: object + diskLastRefAtMap: + additionalProperties: + type: string + nullable: true + type: object + ownerID: + type: string + size: + format: int64 + type: integer + uuid: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + labels: {{- include "longhorn.labels" . | nindent 4 }} + longhorn-manager: "" + name: backups.longhorn.io +spec: + group: longhorn.io + names: + kind: Backup + listKind: BackupList + plural: backups + shortNames: + - lhb + singular: backup + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The snapshot name + jsonPath: .status.snapshotName + name: SnapshotName + type: string + - description: The snapshot size + jsonPath: .status.size + name: SnapshotSize + type: string + - description: The snapshot creation time + jsonPath: .status.snapshotCreatedAt + name: SnapshotCreatedAt + type: string + - description: The backup state + jsonPath: .status.state + name: State + type: string + - description: The backup last synced time + jsonPath: .status.lastSyncedAt + name: LastSyncedAt + type: string + name: v1beta1 + schema: + openAPIV3Schema: + description: Backup is where Longhorn stores backup object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - description: The snapshot name + jsonPath: .status.snapshotName + name: SnapshotName + type: string + - description: The snapshot size + jsonPath: .status.size + name: SnapshotSize + type: string + - description: The snapshot creation time + jsonPath: .status.snapshotCreatedAt + name: SnapshotCreatedAt + type: string + - description: The backup state + jsonPath: .status.state + name: State + type: string + - description: The backup last synced time + jsonPath: .status.lastSyncedAt + name: LastSyncedAt + type: string + name: v1beta2 + schema: + openAPIV3Schema: + description: Backup is where Longhorn stores backup object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: BackupSpec defines the desired state of the Longhorn backup + properties: + labels: + additionalProperties: + type: string + description: The labels of snapshot backup. + type: object + snapshotName: + description: The snapshot name. + type: string + syncRequestedAt: + description: The time to request run sync the remote backup. + format: date-time + nullable: true + type: string + type: object + status: + description: BackupStatus defines the observed state of the Longhorn backup + properties: + backupCreatedAt: + description: The snapshot backup upload finished time. + type: string + compressionMethod: + description: Compression method + type: string + error: + description: The error message when taking the snapshot backup. + type: string + labels: + additionalProperties: + type: string + description: The labels of snapshot backup. + nullable: true + type: object + lastSyncedAt: + description: The last time that the backup was synced with the remote backup target. + format: date-time + nullable: true + type: string + messages: + additionalProperties: + type: string + description: The error messages when calling longhorn engine on listing or inspecting backups. + nullable: true + type: object + ownerID: + description: The node ID on which the controller is responsible to reconcile this backup CR. + type: string + progress: + description: The snapshot backup progress. + type: integer + replicaAddress: + description: The address of the replica that runs snapshot backup. + type: string + size: + description: The snapshot size. + type: string + snapshotCreatedAt: + description: The snapshot creation time. + type: string + snapshotName: + description: The snapshot name. + type: string + state: + description: The backup creation state. Can be "", "InProgress", "Completed", "Error", "Unknown". + type: string + url: + description: The snapshot backup URL. + type: string + volumeBackingImageName: + description: The volume's backing image name. + type: string + volumeCreated: + description: The volume creation time. + type: string + volumeName: + description: The volume name. + type: string + volumeSize: + description: The volume size. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + labels: {{- include "longhorn.labels" . | nindent 4 }} + longhorn-manager: "" + name: backuptargets.longhorn.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + name: longhorn-conversion-webhook + namespace: {{ include "release_namespace" . }} + path: /v1/webhook/conversion + port: 9501 + conversionReviewVersions: + - v1beta2 + - v1beta1 + group: longhorn.io + names: + kind: BackupTarget + listKind: BackupTargetList + plural: backuptargets + shortNames: + - lhbt + singular: backuptarget + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The backup target URL + jsonPath: .spec.backupTargetURL + name: URL + type: string + - description: The backup target credential secret + jsonPath: .spec.credentialSecret + name: Credential + type: string + - description: The backup target poll interval + jsonPath: .spec.pollInterval + name: LastBackupAt + type: string + - description: Indicate whether the backup target is available or not + jsonPath: .status.available + name: Available + type: boolean + - description: The backup target last synced time + jsonPath: .status.lastSyncedAt + name: LastSyncedAt + type: string + name: v1beta1 + schema: + openAPIV3Schema: + description: BackupTarget is where Longhorn stores backup target object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - description: The backup target URL + jsonPath: .spec.backupTargetURL + name: URL + type: string + - description: The backup target credential secret + jsonPath: .spec.credentialSecret + name: Credential + type: string + - description: The backup target poll interval + jsonPath: .spec.pollInterval + name: LastBackupAt + type: string + - description: Indicate whether the backup target is available or not + jsonPath: .status.available + name: Available + type: boolean + - description: The backup target last synced time + jsonPath: .status.lastSyncedAt + name: LastSyncedAt + type: string + name: v1beta2 + schema: + openAPIV3Schema: + description: BackupTarget is where Longhorn stores backup target object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: BackupTargetSpec defines the desired state of the Longhorn backup target + properties: + backupTargetURL: + description: The backup target URL. + type: string + credentialSecret: + description: The backup target credential secret. + type: string + pollInterval: + description: The interval that the cluster needs to run sync with the backup target. + type: string + syncRequestedAt: + description: The time to request run sync the remote backup target. + format: date-time + nullable: true + type: string + type: object + status: + description: BackupTargetStatus defines the observed state of the Longhorn backup target + properties: + available: + description: Available indicates if the remote backup target is available or not. + type: boolean + conditions: + description: Records the reason on why the backup target is unavailable. + items: + properties: + lastProbeTime: + description: Last time we probed the condition. + type: string + lastTransitionTime: + description: Last time the condition transitioned from one status to another. + type: string + message: + description: Human-readable message indicating details about last transition. + type: string + reason: + description: Unique, one-word, CamelCase reason for the condition's last transition. + type: string + status: + description: Status is the status of the condition. Can be True, False, Unknown. + type: string + type: + description: Type is the type of the condition. + type: string + type: object + nullable: true + type: array + lastSyncedAt: + description: The last time that the controller synced with the remote backup target. + format: date-time + nullable: true + type: string + ownerID: + description: The node ID on which the controller is responsible to reconcile this backup target CR. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + labels: {{- include "longhorn.labels" . | nindent 4 }} + longhorn-manager: "" + name: backupvolumes.longhorn.io +spec: + group: longhorn.io + names: + kind: BackupVolume + listKind: BackupVolumeList + plural: backupvolumes + shortNames: + - lhbv + singular: backupvolume + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The backup volume creation time + jsonPath: .status.createdAt + name: CreatedAt + type: string + - description: The backup volume last backup name + jsonPath: .status.lastBackupName + name: LastBackupName + type: string + - description: The backup volume last backup time + jsonPath: .status.lastBackupAt + name: LastBackupAt + type: string + - description: The backup volume last synced time + jsonPath: .status.lastSyncedAt + name: LastSyncedAt + type: string + name: v1beta1 + schema: + openAPIV3Schema: + description: BackupVolume is where Longhorn stores backup volume object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - description: The backup volume creation time + jsonPath: .status.createdAt + name: CreatedAt + type: string + - description: The backup volume last backup name + jsonPath: .status.lastBackupName + name: LastBackupName + type: string + - description: The backup volume last backup time + jsonPath: .status.lastBackupAt + name: LastBackupAt + type: string + - description: The backup volume last synced time + jsonPath: .status.lastSyncedAt + name: LastSyncedAt + type: string + name: v1beta2 + schema: + openAPIV3Schema: + description: BackupVolume is where Longhorn stores backup volume object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: BackupVolumeSpec defines the desired state of the Longhorn backup volume + properties: + syncRequestedAt: + description: The time to request run sync the remote backup volume. + format: date-time + nullable: true + type: string + type: object + status: + description: BackupVolumeStatus defines the observed state of the Longhorn backup volume + properties: + backingImageChecksum: + description: the backing image checksum. + type: string + backingImageName: + description: The backing image name. + type: string + createdAt: + description: The backup volume creation time. + type: string + dataStored: + description: The backup volume block count. + type: string + labels: + additionalProperties: + type: string + description: The backup volume labels. + nullable: true + type: object + lastBackupAt: + description: The latest volume backup time. + type: string + lastBackupName: + description: The latest volume backup name. + type: string + lastModificationTime: + description: The backup volume config last modification time. + format: date-time + nullable: true + type: string + lastSyncedAt: + description: The last time that the backup volume was synced into the cluster. + format: date-time + nullable: true + type: string + messages: + additionalProperties: + type: string + description: The error messages when call longhorn engine on list or inspect backup volumes. + nullable: true + type: object + ownerID: + description: The node ID on which the controller is responsible to reconcile this backup volume CR. + type: string + size: + description: The backup volume size. + type: string + storageClassName: + description: the storage class name of pv/pvc binding with the volume. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + labels: {{- include "longhorn.labels" . | nindent 4 }} + longhorn-manager: "" + name: engineimages.longhorn.io +spec: + preserveUnknownFields: false + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + name: longhorn-conversion-webhook + namespace: {{ include "release_namespace" . }} + path: /v1/webhook/conversion + port: 9501 + conversionReviewVersions: + - v1beta2 + - v1beta1 + group: longhorn.io + names: + kind: EngineImage + listKind: EngineImageList + plural: engineimages + shortNames: + - lhei + singular: engineimage + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: State of the engine image + jsonPath: .status.state + name: State + type: string + - description: The Longhorn engine image + jsonPath: .spec.image + name: Image + type: string + - description: Number of resources using the engine image + jsonPath: .status.refCount + name: RefCount + type: integer + - description: The build date of the engine image + jsonPath: .status.buildDate + name: BuildDate + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: EngineImage is where Longhorn stores engine image object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - description: State of the engine image + jsonPath: .status.state + name: State + type: string + - description: The Longhorn engine image + jsonPath: .spec.image + name: Image + type: string + - description: Number of resources using the engine image + jsonPath: .status.refCount + name: RefCount + type: integer + - description: The build date of the engine image + jsonPath: .status.buildDate + name: BuildDate + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta2 + schema: + openAPIV3Schema: + description: EngineImage is where Longhorn stores engine image object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: EngineImageSpec defines the desired state of the Longhorn engine image + properties: + image: + minLength: 1 + type: string + required: + - image + type: object + status: + description: EngineImageStatus defines the observed state of the Longhorn engine image + properties: + buildDate: + type: string + cliAPIMinVersion: + type: integer + cliAPIVersion: + type: integer + conditions: + items: + properties: + lastProbeTime: + description: Last time we probed the condition. + type: string + lastTransitionTime: + description: Last time the condition transitioned from one status to another. + type: string + message: + description: Human-readable message indicating details about last transition. + type: string + reason: + description: Unique, one-word, CamelCase reason for the condition's last transition. + type: string + status: + description: Status is the status of the condition. Can be True, False, Unknown. + type: string + type: + description: Type is the type of the condition. + type: string + type: object + nullable: true + type: array + controllerAPIMinVersion: + type: integer + controllerAPIVersion: + type: integer + dataFormatMinVersion: + type: integer + dataFormatVersion: + type: integer + gitCommit: + type: string + noRefSince: + type: string + nodeDeploymentMap: + additionalProperties: + type: boolean + nullable: true + type: object + ownerID: + type: string + refCount: + type: integer + state: + type: string + version: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + labels: {{- include "longhorn.labels" . | nindent 4 }} + longhorn-manager: "" + name: engines.longhorn.io +spec: + group: longhorn.io + names: + kind: Engine + listKind: EngineList + plural: engines + shortNames: + - lhe + singular: engine + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The current state of the engine + jsonPath: .status.currentState + name: State + type: string + - description: The node that the engine is on + jsonPath: .spec.nodeID + name: Node + type: string + - description: The instance manager of the engine + jsonPath: .status.instanceManagerName + name: InstanceManager + type: string + - description: The current image of the engine + jsonPath: .status.currentImage + name: Image + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: Engine is where Longhorn stores engine object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - description: The current state of the engine + jsonPath: .status.currentState + name: State + type: string + - description: The node that the engine is on + jsonPath: .spec.nodeID + name: Node + type: string + - description: The instance manager of the engine + jsonPath: .status.instanceManagerName + name: InstanceManager + type: string + - description: The current image of the engine + jsonPath: .status.currentImage + name: Image + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta2 + schema: + openAPIV3Schema: + description: Engine is where Longhorn stores engine object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: EngineSpec defines the desired state of the Longhorn engine + properties: + active: + type: boolean + backendStoreDriver: + enum: + - v1 + - v2 + type: string + backupVolume: + type: string + desireState: + type: string + disableFrontend: + type: boolean + engineImage: + type: string + frontend: + enum: + - blockdev + - iscsi + - nvmf + - "" + type: string + logRequested: + type: boolean + nodeID: + type: string + replicaAddressMap: + additionalProperties: + type: string + type: object + requestedBackupRestore: + type: string + requestedDataSource: + type: string + revisionCounterDisabled: + type: boolean + salvageRequested: + type: boolean + unmapMarkSnapChainRemovedEnabled: + type: boolean + upgradedReplicaAddressMap: + additionalProperties: + type: string + type: object + volumeName: + type: string + volumeSize: + format: int64 + type: string + type: object + status: + description: EngineStatus defines the observed state of the Longhorn engine + properties: + backupStatus: + additionalProperties: + properties: + backupURL: + type: string + error: + type: string + progress: + type: integer + replicaAddress: + type: string + snapshotName: + type: string + state: + type: string + type: object + nullable: true + type: object + cloneStatus: + additionalProperties: + properties: + error: + type: string + fromReplicaAddress: + type: string + isCloning: + type: boolean + progress: + type: integer + snapshotName: + type: string + state: + type: string + type: object + nullable: true + type: object + conditions: + items: + properties: + lastProbeTime: + description: Last time we probed the condition. + type: string + lastTransitionTime: + description: Last time the condition transitioned from one status to another. + type: string + message: + description: Human-readable message indicating details about last transition. + type: string + reason: + description: Unique, one-word, CamelCase reason for the condition's last transition. + type: string + status: + description: Status is the status of the condition. Can be True, False, Unknown. + type: string + type: + description: Type is the type of the condition. + type: string + type: object + nullable: true + type: array + currentImage: + type: string + currentReplicaAddressMap: + additionalProperties: + type: string + nullable: true + type: object + currentSize: + format: int64 + type: string + currentState: + type: string + endpoint: + type: string + instanceManagerName: + type: string + ip: + type: string + isExpanding: + type: boolean + lastExpansionError: + type: string + lastExpansionFailedAt: + type: string + lastRestoredBackup: + type: string + logFetched: + type: boolean + ownerID: + type: string + port: + type: integer + purgeStatus: + additionalProperties: + properties: + error: + type: string + isPurging: + type: boolean + progress: + type: integer + state: + type: string + type: object + nullable: true + type: object + rebuildStatus: + additionalProperties: + properties: + error: + type: string + fromReplicaAddress: + type: string + isRebuilding: + type: boolean + progress: + type: integer + state: + type: string + type: object + nullable: true + type: object + replicaModeMap: + additionalProperties: + type: string + nullable: true + type: object + restoreStatus: + additionalProperties: + properties: + backupURL: + type: string + currentRestoringBackup: + type: string + error: + type: string + filename: + type: string + isRestoring: + type: boolean + lastRestored: + type: string + progress: + type: integer + state: + type: string + type: object + nullable: true + type: object + salvageExecuted: + type: boolean + snapshots: + additionalProperties: + properties: + children: + additionalProperties: + type: boolean + nullable: true + type: object + created: + type: string + labels: + additionalProperties: + type: string + nullable: true + type: object + name: + type: string + parent: + type: string + removed: + type: boolean + size: + type: string + usercreated: + type: boolean + type: object + nullable: true + type: object + snapshotsError: + type: string + started: + type: boolean + storageIP: + type: string + unmapMarkSnapChainRemovedEnabled: + type: boolean + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + labels: {{- include "longhorn.labels" . | nindent 4 }} + longhorn-manager: "" + name: instancemanagers.longhorn.io +spec: + group: longhorn.io + names: + kind: InstanceManager + listKind: InstanceManagerList + plural: instancemanagers + shortNames: + - lhim + singular: instancemanager + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The state of the instance manager + jsonPath: .status.currentState + name: State + type: string + - description: The type of the instance manager (engine or replica) + jsonPath: .spec.type + name: Type + type: string + - description: The node that the instance manager is running on + jsonPath: .spec.nodeID + name: Node + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: InstanceManager is where Longhorn stores instance manager object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - description: The state of the instance manager + jsonPath: .status.currentState + name: State + type: string + - description: The type of the instance manager (engine or replica) + jsonPath: .spec.type + name: Type + type: string + - description: The node that the instance manager is running on + jsonPath: .spec.nodeID + name: Node + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta2 + schema: + openAPIV3Schema: + description: InstanceManager is where Longhorn stores instance manager object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: InstanceManagerSpec defines the desired state of the Longhorn instancer manager + properties: + image: + type: string + nodeID: + type: string + type: + enum: + - aio + - engine + - replica + type: string + type: object + status: + description: InstanceManagerStatus defines the observed state of the Longhorn instance manager + properties: + apiMinVersion: + type: integer + apiVersion: + type: integer + proxyApiMinVersion: + type: integer + proxyApiVersion: + type: integer + currentState: + type: string + instanceEngines: + additionalProperties: + properties: + spec: + properties: + backendStoreDriver: + type: string + name: + type: string + type: object + status: + properties: + endpoint: + type: string + errorMsg: + type: string + listen: + type: string + portEnd: + format: int32 + type: integer + portStart: + format: int32 + type: integer + resourceVersion: + format: int64 + type: integer + state: + type: string + type: + type: string + type: object + type: object + nullable: true + type: object + instanceReplicas: + additionalProperties: + properties: + spec: + properties: + backendStoreDriver: + type: string + name: + type: string + type: object + status: + properties: + endpoint: + type: string + errorMsg: + type: string + listen: + type: string + portEnd: + format: int32 + type: integer + portStart: + format: int32 + type: integer + resourceVersion: + format: int64 + type: integer + state: + type: string + type: + type: string + type: object + type: object + nullable: true + type: object + instances: + additionalProperties: + properties: + spec: + properties: + backendStoreDriver: + type: string + name: + type: string + type: object + status: + properties: + endpoint: + type: string + errorMsg: + type: string + listen: + type: string + portEnd: + format: int32 + type: integer + portStart: + format: int32 + type: integer + resourceVersion: + format: int64 + type: integer + state: + type: string + type: + type: string + type: object + type: object + nullable: true + description: 'Deprecated: Replaced by InstanceEngines and InstanceReplicas' + type: object + ip: + type: string + ownerID: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + labels: {{- include "longhorn.labels" . | nindent 4 }} + longhorn-manager: "" + name: nodes.longhorn.io +spec: + preserveUnknownFields: false + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + name: longhorn-conversion-webhook + namespace: {{ include "release_namespace" . }} + path: /v1/webhook/conversion + port: 9501 + conversionReviewVersions: + - v1beta2 + - v1beta1 + group: longhorn.io + names: + kind: Node + listKind: NodeList + plural: nodes + shortNames: + - lhn + singular: node + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Indicate whether the node is ready + jsonPath: .status.conditions['Ready']['status'] + name: Ready + type: string + - description: Indicate whether the user disabled/enabled replica scheduling for the node + jsonPath: .spec.allowScheduling + name: AllowScheduling + type: boolean + - description: Indicate whether Longhorn can schedule replicas on the node + jsonPath: .status.conditions['Schedulable']['status'] + name: Schedulable + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: Node is where Longhorn stores Longhorn node object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - description: Indicate whether the node is ready + jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Ready + type: string + - description: Indicate whether the user disabled/enabled replica scheduling for the node + jsonPath: .spec.allowScheduling + name: AllowScheduling + type: boolean + - description: Indicate whether Longhorn can schedule replicas on the node + jsonPath: .status.conditions[?(@.type=='Schedulable')].status + name: Schedulable + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta2 + schema: + openAPIV3Schema: + description: Node is where Longhorn stores Longhorn node object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: NodeSpec defines the desired state of the Longhorn node + properties: + allowScheduling: + type: boolean + disks: + additionalProperties: + properties: + allowScheduling: + type: boolean + evictionRequested: + type: boolean + path: + type: string + storageReserved: + format: int64 + type: integer + tags: + items: + type: string + type: array + diskType: + enum: + - filesystem + - block + type: string + type: object + type: object + evictionRequested: + type: boolean + instanceManagerCPURequest: + type: integer + name: + type: string + tags: + items: + type: string + type: array + type: object + status: + description: NodeStatus defines the observed state of the Longhorn node + properties: + conditions: + items: + properties: + lastProbeTime: + description: Last time we probed the condition. + type: string + lastTransitionTime: + description: Last time the condition transitioned from one status to another. + type: string + message: + description: Human-readable message indicating details about last transition. + type: string + reason: + description: Unique, one-word, CamelCase reason for the condition's last transition. + type: string + status: + description: Status is the status of the condition. Can be True, False, Unknown. + type: string + type: + description: Type is the type of the condition. + type: string + type: object + nullable: true + type: array + diskStatus: + additionalProperties: + properties: + conditions: + items: + properties: + lastProbeTime: + description: Last time we probed the condition. + type: string + lastTransitionTime: + description: Last time the condition transitioned from one status to another. + type: string + message: + description: Human-readable message indicating details about last transition. + type: string + reason: + description: Unique, one-word, CamelCase reason for the condition's last transition. + type: string + status: + description: Status is the status of the condition. Can be True, False, Unknown. + type: string + type: + description: Type is the type of the condition. + type: string + type: object + nullable: true + type: array + diskType: + type: string + diskUUID: + type: string + scheduledReplica: + additionalProperties: + format: int64 + type: integer + nullable: true + type: object + storageAvailable: + format: int64 + type: integer + storageMaximum: + format: int64 + type: integer + storageScheduled: + format: int64 + type: integer + type: object + nullable: true + type: object + region: + type: string + snapshotCheckStatus: + properties: + lastPeriodicCheckedAt: + format: date-time + type: string + snapshotCheckState: + type: string + type: object + zone: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + labels: {{- include "longhorn.labels" . | nindent 4 }} + longhorn-manager: "" + name: orphans.longhorn.io +spec: + group: longhorn.io + names: + kind: Orphan + listKind: OrphanList + plural: orphans + shortNames: + - lho + singular: orphan + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The type of the orphan + jsonPath: .spec.orphanType + name: Type + type: string + - description: The node that the orphan is on + jsonPath: .spec.nodeID + name: Node + type: string + name: v1beta2 + schema: + openAPIV3Schema: + description: Orphan is where Longhorn stores orphan object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: OrphanSpec defines the desired state of the Longhorn orphaned data + properties: + nodeID: + description: The node ID on which the controller is responsible to reconcile this orphan CR. + type: string + orphanType: + description: The type of the orphaned data. Can be "replica". + type: string + parameters: + additionalProperties: + type: string + description: The parameters of the orphaned data + type: object + type: object + status: + description: OrphanStatus defines the observed state of the Longhorn orphaned data + properties: + conditions: + items: + properties: + lastProbeTime: + description: Last time we probed the condition. + type: string + lastTransitionTime: + description: Last time the condition transitioned from one status to another. + type: string + message: + description: Human-readable message indicating details about last transition. + type: string + reason: + description: Unique, one-word, CamelCase reason for the condition's last transition. + type: string + status: + description: Status is the status of the condition. Can be True, False, Unknown. + type: string + type: + description: Type is the type of the condition. + type: string + type: object + nullable: true + type: array + ownerID: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + labels: + longhorn-manager: "" + name: recurringjobs.longhorn.io +spec: + group: longhorn.io + names: + kind: RecurringJob + listKind: RecurringJobList + plural: recurringjobs + shortNames: + - lhrj + singular: recurringjob + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Sets groupings to the jobs. When set to "default" group will be added to the volume label when no other job label exist in volume + jsonPath: .spec.groups + name: Groups + type: string + - description: Should be one of "backup" or "snapshot" + jsonPath: .spec.task + name: Task + type: string + - description: The cron expression represents recurring job scheduling + jsonPath: .spec.cron + name: Cron + type: string + - description: The number of snapshots/backups to keep for the volume + jsonPath: .spec.retain + name: Retain + type: integer + - description: The concurrent job to run by each cron job + jsonPath: .spec.concurrency + name: Concurrency + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - description: Specify the labels + jsonPath: .spec.labels + name: Labels + type: string + name: v1beta1 + schema: + openAPIV3Schema: + description: RecurringJob is where Longhorn stores recurring job object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - description: Sets groupings to the jobs. When set to "default" group will be added to the volume label when no other job label exist in volume + jsonPath: .spec.groups + name: Groups + type: string + - description: Should be one of "snapshot", "snapshot-force-create", "snapshot-cleanup", "snapshot-delete", "backup", "backup-force-create" or "filesystem-trim" + jsonPath: .spec.task + name: Task + type: string + - description: The cron expression represents recurring job scheduling + jsonPath: .spec.cron + name: Cron + type: string + - description: The number of snapshots/backups to keep for the volume + jsonPath: .spec.retain + name: Retain + type: integer + - description: The concurrent job to run by each cron job + jsonPath: .spec.concurrency + name: Concurrency + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - description: Specify the labels + jsonPath: .spec.labels + name: Labels + type: string + name: v1beta2 + schema: + openAPIV3Schema: + description: RecurringJob is where Longhorn stores recurring job object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: RecurringJobSpec defines the desired state of the Longhorn recurring job + properties: + concurrency: + description: The concurrency of taking the snapshot/backup. + type: integer + cron: + description: The cron setting. + type: string + groups: + description: The recurring job group. + items: + type: string + type: array + labels: + additionalProperties: + type: string + description: The label of the snapshot/backup. + type: object + name: + description: The recurring job name. + type: string + retain: + description: The retain count of the snapshot/backup. + type: integer + task: + description: The recurring job task. Can be "snapshot", "snapshot-force-create", "snapshot-cleanup", "snapshot-delete", "backup", "backup-force-create" or "filesystem-trim" + enum: + - snapshot + - snapshot-force-create + - snapshot-cleanup + - snapshot-delete + - backup + - backup-force-create + - filesystem-trim + type: string + type: object + status: + description: RecurringJobStatus defines the observed state of the Longhorn recurring job + properties: + ownerID: + description: The owner ID which is responsible to reconcile this recurring job CR. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + labels: {{- include "longhorn.labels" . | nindent 4 }} + longhorn-manager: "" + name: replicas.longhorn.io +spec: + group: longhorn.io + names: + kind: Replica + listKind: ReplicaList + plural: replicas + shortNames: + - lhr + singular: replica + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The current state of the replica + jsonPath: .status.currentState + name: State + type: string + - description: The node that the replica is on + jsonPath: .spec.nodeID + name: Node + type: string + - description: The disk that the replica is on + jsonPath: .spec.diskID + name: Disk + type: string + - description: The instance manager of the replica + jsonPath: .status.instanceManagerName + name: InstanceManager + type: string + - description: The current image of the replica + jsonPath: .status.currentImage + name: Image + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: Replica is where Longhorn stores replica object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - description: The current state of the replica + jsonPath: .status.currentState + name: State + type: string + - description: The node that the replica is on + jsonPath: .spec.nodeID + name: Node + type: string + - description: The disk that the replica is on + jsonPath: .spec.diskID + name: Disk + type: string + - description: The instance manager of the replica + jsonPath: .status.instanceManagerName + name: InstanceManager + type: string + - description: The current image of the replica + jsonPath: .status.currentImage + name: Image + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta2 + schema: + openAPIV3Schema: + description: Replica is where Longhorn stores replica object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ReplicaSpec defines the desired state of the Longhorn replica + properties: + active: + type: boolean + backendStoreDriver: + enum: + - v1 + - v2 + type: string + backingImage: + type: string + dataDirectoryName: + type: string + desireState: + type: string + diskID: + type: string + diskPath: + type: string + engineImage: + type: string + engineName: + type: string + failedAt: + type: string + hardNodeAffinity: + type: string + healthyAt: + type: string + logRequested: + type: boolean + nodeID: + type: string + rebuildRetryCount: + type: integer + revisionCounterDisabled: + type: boolean + salvageRequested: + type: boolean + unmapMarkDiskChainRemovedEnabled: + type: boolean + volumeName: + type: string + volumeSize: + format: int64 + type: string + type: object + status: + description: ReplicaStatus defines the observed state of the Longhorn replica + properties: + conditions: + items: + properties: + lastProbeTime: + description: Last time we probed the condition. + type: string + lastTransitionTime: + description: Last time the condition transitioned from one status to another. + type: string + message: + description: Human-readable message indicating details about last transition. + type: string + reason: + description: Unique, one-word, CamelCase reason for the condition's last transition. + type: string + status: + description: Status is the status of the condition. Can be True, False, Unknown. + type: string + type: + description: Type is the type of the condition. + type: string + type: object + nullable: true + type: array + currentImage: + type: string + currentState: + type: string + evictionRequested: + type: boolean + instanceManagerName: + type: string + ip: + type: string + logFetched: + type: boolean + ownerID: + type: string + port: + type: integer + salvageExecuted: + type: boolean + started: + type: boolean + storageIP: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + labels: {{- include "longhorn.labels" . | nindent 4 }} + longhorn-manager: "" + name: settings.longhorn.io +spec: + group: longhorn.io + names: + kind: Setting + listKind: SettingList + plural: settings + shortNames: + - lhs + singular: setting + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The value of the setting + jsonPath: .value + name: Value + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: Setting is where Longhorn stores setting object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + value: + type: string + required: + - value + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - description: The value of the setting + jsonPath: .value + name: Value + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta2 + schema: + openAPIV3Schema: + description: Setting is where Longhorn stores setting object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + value: + type: string + required: + - value + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + labels: {{- include "longhorn.labels" . | nindent 4 }} + longhorn-manager: "" + name: sharemanagers.longhorn.io +spec: + group: longhorn.io + names: + kind: ShareManager + listKind: ShareManagerList + plural: sharemanagers + shortNames: + - lhsm + singular: sharemanager + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The state of the share manager + jsonPath: .status.state + name: State + type: string + - description: The node that the share manager is owned by + jsonPath: .status.ownerID + name: Node + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: ShareManager is where Longhorn stores share manager object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - description: The state of the share manager + jsonPath: .status.state + name: State + type: string + - description: The node that the share manager is owned by + jsonPath: .status.ownerID + name: Node + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta2 + schema: + openAPIV3Schema: + description: ShareManager is where Longhorn stores share manager object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ShareManagerSpec defines the desired state of the Longhorn share manager + properties: + image: + description: Share manager image used for creating a share manager pod + type: string + type: object + status: + description: ShareManagerStatus defines the observed state of the Longhorn share manager + properties: + endpoint: + description: NFS endpoint that can access the mounted filesystem of the volume + type: string + ownerID: + description: The node ID on which the controller is responsible to reconcile this share manager resource + type: string + state: + description: The state of the share manager resource + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + labels: {{- include "longhorn.labels" . | nindent 4 }} + longhorn-manager: "" + name: snapshots.longhorn.io +spec: + group: longhorn.io + names: + kind: Snapshot + listKind: SnapshotList + plural: snapshots + shortNames: + - lhsnap + singular: snapshot + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The volume that this snapshot belongs to + jsonPath: .spec.volume + name: Volume + type: string + - description: Timestamp when the point-in-time snapshot was taken + jsonPath: .status.creationTime + name: CreationTime + type: string + - description: Indicates if the snapshot is ready to be used to restore/backup a volume + jsonPath: .status.readyToUse + name: ReadyToUse + type: boolean + - description: Represents the minimum size of volume required to rehydrate from this snapshot + jsonPath: .status.restoreSize + name: RestoreSize + type: string + - description: The actual size of the snapshot + jsonPath: .status.size + name: Size + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta2 + schema: + openAPIV3Schema: + description: Snapshot is the Schema for the snapshots API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SnapshotSpec defines the desired state of Longhorn Snapshot + properties: + createSnapshot: + description: require creating a new snapshot + type: boolean + labels: + additionalProperties: + type: string + description: The labels of snapshot + nullable: true + type: object + volume: + description: the volume that this snapshot belongs to. This field is immutable after creation. Required + type: string + required: + - volume + type: object + status: + description: SnapshotStatus defines the observed state of Longhorn Snapshot + properties: + checksum: + type: string + children: + additionalProperties: + type: boolean + nullable: true + type: object + creationTime: + type: string + error: + type: string + labels: + additionalProperties: + type: string + nullable: true + type: object + markRemoved: + type: boolean + ownerID: + type: string + parent: + type: string + readyToUse: + type: boolean + restoreSize: + format: int64 + type: integer + size: + format: int64 + type: integer + userCreated: + type: boolean + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + labels: {{- include "longhorn.labels" . | nindent 4 }} + longhorn-manager: "" + name: supportbundles.longhorn.io +spec: + group: longhorn.io + names: + kind: SupportBundle + listKind: SupportBundleList + plural: supportbundles + shortNames: + - lhbundle + singular: supportbundle + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The state of the support bundle + jsonPath: .status.state + name: State + type: string + - description: The issue URL + jsonPath: .spec.issueURL + name: Issue + type: string + - description: A brief description of the issue + jsonPath: .spec.description + name: Description + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta2 + schema: + openAPIV3Schema: + description: SupportBundle is where Longhorn stores support bundle object + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SupportBundleSpec defines the desired state of the Longhorn SupportBundle + properties: + description: + description: A brief description of the issue + type: string + issueURL: + description: The issue URL + nullable: true + type: string + nodeID: + description: The preferred responsible controller node ID. + type: string + required: + - description + type: object + status: + description: SupportBundleStatus defines the observed state of the Longhorn SupportBundle + properties: + conditions: + items: + properties: + lastProbeTime: + description: Last time we probed the condition. + type: string + lastTransitionTime: + description: Last time the condition transitioned from one status to another. + type: string + message: + description: Human-readable message indicating details about last transition. + type: string + reason: + description: Unique, one-word, CamelCase reason for the condition's last transition. + type: string + status: + description: Status is the status of the condition. Can be True, False, Unknown. + type: string + type: + description: Type is the type of the condition. + type: string + type: object + type: array + filename: + type: string + filesize: + format: int64 + type: integer + image: + description: The support bundle manager image + type: string + managerIP: + description: The support bundle manager IP + type: string + ownerID: + description: The current responsible controller node ID + type: string + progress: + type: integer + state: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + labels: {{- include "longhorn.labels" . | nindent 4 }} + longhorn-manager: "" + name: systembackups.longhorn.io +spec: + group: longhorn.io + names: + kind: SystemBackup + listKind: SystemBackupList + plural: systembackups + shortNames: + - lhsb + singular: systembackup + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The system backup Longhorn version + jsonPath: .status.version + name: Version + type: string + - description: The system backup state + jsonPath: .status.state + name: State + type: string + - description: The system backup creation time + jsonPath: .status.createdAt + name: Created + type: string + - description: The last time that the system backup was synced into the cluster + jsonPath: .status.lastSyncedAt + name: LastSyncedAt + type: string + name: v1beta2 + schema: + openAPIV3Schema: + description: SystemBackup is where Longhorn stores system backup object + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SystemBackupSpec defines the desired state of the Longhorn SystemBackup + properties: + volumeBackupPolicy: + description: The create volume backup policy Can be "if-not-present", "always" or "disabled" + nullable: true + type: string + type: object + status: + description: SystemBackupStatus defines the observed state of the Longhorn SystemBackup + properties: + conditions: + items: + properties: + lastProbeTime: + description: Last time we probed the condition. + type: string + lastTransitionTime: + description: Last time the condition transitioned from one status to another. + type: string + message: + description: Human-readable message indicating details about last transition. + type: string + reason: + description: Unique, one-word, CamelCase reason for the condition's last transition. + type: string + status: + description: Status is the status of the condition. Can be True, False, Unknown. + type: string + type: + description: Type is the type of the condition. + type: string + type: object + nullable: true + type: array + createdAt: + description: The system backup creation time. + format: date-time + type: string + gitCommit: + description: The saved Longhorn manager git commit. + nullable: true + type: string + lastSyncedAt: + description: The last time that the system backup was synced into the cluster. + format: date-time + nullable: true + type: string + managerImage: + description: The saved manager image. + type: string + ownerID: + description: The node ID of the responsible controller to reconcile this SystemBackup. + type: string + state: + description: The system backup state. + type: string + version: + description: The saved Longhorn version. + nullable: true + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + labels: {{- include "longhorn.labels" . | nindent 4 }} + longhorn-manager: "" + name: systemrestores.longhorn.io +spec: + group: longhorn.io + names: + kind: SystemRestore + listKind: SystemRestoreList + plural: systemrestores + shortNames: + - lhsr + singular: systemrestore + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The system restore state + jsonPath: .status.state + name: State + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta2 + schema: + openAPIV3Schema: + description: SystemRestore is where Longhorn stores system restore object + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SystemRestoreSpec defines the desired state of the Longhorn SystemRestore + properties: + systemBackup: + description: The system backup name in the object store. + type: string + required: + - systemBackup + type: object + status: + description: SystemRestoreStatus defines the observed state of the Longhorn SystemRestore + properties: + conditions: + items: + properties: + lastProbeTime: + description: Last time we probed the condition. + type: string + lastTransitionTime: + description: Last time the condition transitioned from one status to another. + type: string + message: + description: Human-readable message indicating details about last transition. + type: string + reason: + description: Unique, one-word, CamelCase reason for the condition's last transition. + type: string + status: + description: Status is the status of the condition. Can be True, False, Unknown. + type: string + type: + description: Type is the type of the condition. + type: string + type: object + nullable: true + type: array + ownerID: + description: The node ID of the responsible controller to reconcile this SystemRestore. + type: string + sourceURL: + description: The source system backup URL. + type: string + state: + description: The system restore state. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + labels: {{- include "longhorn.labels" . | nindent 4 }} + longhorn-manager: "" + name: volumes.longhorn.io +spec: + preserveUnknownFields: false + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + name: longhorn-conversion-webhook + namespace: {{ include "release_namespace" . }} + path: /v1/webhook/conversion + port: 9501 + conversionReviewVersions: + - v1beta2 + - v1beta1 + group: longhorn.io + names: + kind: Volume + listKind: VolumeList + plural: volumes + shortNames: + - lhv + singular: volume + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The state of the volume + jsonPath: .status.state + name: State + type: string + - description: The robustness of the volume + jsonPath: .status.robustness + name: Robustness + type: string + - description: The scheduled condition of the volume + jsonPath: .status.conditions['scheduled']['status'] + name: Scheduled + type: string + - description: The size of the volume + jsonPath: .spec.size + name: Size + type: string + - description: The node that the volume is currently attaching to + jsonPath: .status.currentNodeID + name: Node + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: Volume is where Longhorn stores volume object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + x-kubernetes-preserve-unknown-fields: true + status: + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - description: The state of the volume + jsonPath: .status.state + name: State + type: string + - description: The robustness of the volume + jsonPath: .status.robustness + name: Robustness + type: string + - description: The scheduled condition of the volume + jsonPath: .status.conditions[?(@.type=='Schedulable')].status + name: Scheduled + type: string + - description: The size of the volume + jsonPath: .spec.size + name: Size + type: string + - description: The node that the volume is currently attaching to + jsonPath: .status.currentNodeID + name: Node + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta2 + schema: + openAPIV3Schema: + description: Volume is where Longhorn stores volume object. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: VolumeSpec defines the desired state of the Longhorn volume + properties: + Standby: + type: boolean + accessMode: + enum: + - rwo + - rwx + type: string + backendStoreDriver: + enum: + - v1 + - v2 + type: string + backingImage: + type: string + backupCompressionMethod: + enum: + - none + - lz4 + - gzip + type: string + dataLocality: + enum: + - disabled + - best-effort + - strict-local + type: string + dataSource: + type: string + disableFrontend: + type: boolean + diskSelector: + items: + type: string + type: array + encrypted: + type: boolean + engineImage: + type: string + fromBackup: + type: string + frontend: + enum: + - blockdev + - iscsi + - nvmf + - "" + type: string + lastAttachedBy: + type: string + migratable: + type: boolean + migrationNodeID: + type: string + nodeID: + type: string + nodeSelector: + items: + type: string + type: array + numberOfReplicas: + type: integer + offlineReplicaRebuilding: + description: OfflineReplicaRebuilding is used to determine if the offline replica rebuilding feature is enabled or not + enum: + - ignored + - disabled + - enabled + type: string + replicaAutoBalance: + enum: + - ignored + - disabled + - least-effort + - best-effort + type: string + replicaSoftAntiAffinity: + description: Replica soft anti affinity of the volume. Set enabled to allow replicas to be scheduled on the same node + enum: + - ignored + - enabled + - disabled + type: string + replicaZoneSoftAntiAffinity: + description: Replica zone soft anti affinity of the volume. Set enabled to allow replicas to be scheduled in the same zone + enum: + - ignored + - enabled + - disabled + type: string + restoreVolumeRecurringJob: + enum: + - ignored + - enabled + - disabled + type: string + revisionCounterDisabled: + type: boolean + size: + format: int64 + type: string + snapshotDataIntegrity: + enum: + - ignored + - disabled + - enabled + - fast-check + type: string + staleReplicaTimeout: + type: integer + unmapMarkSnapChainRemoved: + enum: + - ignored + - disabled + - enabled + type: string + type: object + status: + description: VolumeStatus defines the observed state of the Longhorn volume + properties: + actualSize: + format: int64 + type: integer + cloneStatus: + properties: + snapshot: + type: string + sourceVolume: + type: string + state: + type: string + type: object + conditions: + items: + properties: + lastProbeTime: + description: Last time we probed the condition. + type: string + lastTransitionTime: + description: Last time the condition transitioned from one status to another. + type: string + message: + description: Human-readable message indicating details about last transition. + type: string + reason: + description: Unique, one-word, CamelCase reason for the condition's last transition. + type: string + status: + description: Status is the status of the condition. Can be True, False, Unknown. + type: string + type: + description: Type is the type of the condition. + type: string + type: object + nullable: true + type: array + currentImage: + type: string + currentMigrationNodeID: + description: the node that this volume is currently migrating to + type: string + currentNodeID: + type: string + expansionRequired: + type: boolean + frontendDisabled: + type: boolean + isStandby: + type: boolean + kubernetesStatus: + properties: + lastPVCRefAt: + type: string + lastPodRefAt: + type: string + namespace: + description: determine if PVC/Namespace is history or not + type: string + pvName: + type: string + pvStatus: + type: string + pvcName: + type: string + workloadsStatus: + description: determine if Pod/Workload is history or not + items: + properties: + podName: + type: string + podStatus: + type: string + workloadName: + type: string + workloadType: + type: string + type: object + nullable: true + type: array + type: object + lastBackup: + type: string + lastBackupAt: + type: string + lastDegradedAt: + type: string + offlineReplicaRebuildingRequired: + type: boolean + ownerID: + type: string + pendingNodeID: + description: Deprecated. + type: string + remountRequestedAt: + type: string + restoreInitiated: + type: boolean + restoreRequired: + type: boolean + robustness: + type: string + shareEndpoint: + type: string + shareState: + type: string + state: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + labels: {{- include "longhorn.labels" . | nindent 4 }} + longhorn-manager: "" + name: volumeattachments.longhorn.io +spec: + group: longhorn.io + names: + kind: VolumeAttachment + listKind: VolumeAttachmentList + plural: volumeattachments + shortNames: + - lhva + singular: volumeattachment + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta2 + schema: + openAPIV3Schema: + description: VolumeAttachment stores attachment information of a Longhorn volume + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: VolumeAttachmentSpec defines the desired state of Longhorn VolumeAttachment + properties: + attachmentTickets: + additionalProperties: + properties: + generation: + description: A sequence number representing a specific generation of the desired state. Populated by the system. Read-only. + format: int64 + type: integer + id: + description: The unique ID of this attachment. Used to differentiate different attachments of the same volume. + type: string + nodeID: + description: The node that this attachment is requesting + type: string + parameters: + additionalProperties: + type: string + description: Optional additional parameter for this attachment + type: object + type: + type: string + type: object + type: object + volume: + description: The name of Longhorn volume of this VolumeAttachment + type: string + required: + - volume + type: object + status: + description: VolumeAttachmentStatus defines the observed state of Longhorn VolumeAttachment + properties: + attachmentTicketStatuses: + additionalProperties: + properties: + conditions: + description: Record any error when trying to fulfill this attachment + items: + properties: + lastProbeTime: + description: Last time we probed the condition. + type: string + lastTransitionTime: + description: Last time the condition transitioned from one status to another. + type: string + message: + description: Human-readable message indicating details about last transition. + type: string + reason: + description: Unique, one-word, CamelCase reason for the condition's last transition. + type: string + status: + description: Status is the status of the condition. Can be True, False, Unknown. + type: string + type: + description: Type is the type of the condition. + type: string + type: object + nullable: true + type: array + generation: + description: A sequence number representing a specific generation of the desired state. Populated by the system. Read-only. + format: int64 + type: integer + id: + description: The unique ID of this attachment. Used to differentiate different attachments of the same volume. + type: string + satisfied: + description: Indicate whether this attachment ticket has been satisfied + type: boolean + required: + - conditions + - satisfied + type: object + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/daemonset-sa.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/daemonset-sa.yaml new file mode 100644 index 0000000..f361d27 --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/daemonset-sa.yaml @@ -0,0 +1,151 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + labels: {{- include "longhorn.labels" . | nindent 4 }} + app: longhorn-manager + name: longhorn-manager + namespace: {{ include "release_namespace" . }} +spec: + selector: + matchLabels: + app: longhorn-manager + template: + metadata: + labels: {{- include "longhorn.labels" . | nindent 8 }} + app: longhorn-manager + {{- with .Values.annotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + containers: + - name: longhorn-manager + image: {{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: + privileged: true + command: + - longhorn-manager + - -d + {{- if eq .Values.longhornManager.log.format "json" }} + - -j + {{- end }} + - daemon + - --engine-image + - "{{ template "registry_url" . }}{{ .Values.image.longhorn.engine.repository }}:{{ .Values.image.longhorn.engine.tag }}" + - --instance-manager-image + - "{{ template "registry_url" . }}{{ .Values.image.longhorn.instanceManager.repository }}:{{ .Values.image.longhorn.instanceManager.tag }}" + - --share-manager-image + - "{{ template "registry_url" . }}{{ .Values.image.longhorn.shareManager.repository }}:{{ .Values.image.longhorn.shareManager.tag }}" + - --backing-image-manager-image + - "{{ template "registry_url" . }}{{ .Values.image.longhorn.backingImageManager.repository }}:{{ .Values.image.longhorn.backingImageManager.tag }}" + - --support-bundle-manager-image + - "{{ template "registry_url" . }}{{ .Values.image.longhorn.supportBundleKit.repository }}:{{ .Values.image.longhorn.supportBundleKit.tag }}" + - --manager-image + - "{{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }}" + - --service-account + - longhorn-service-account + ports: + - containerPort: 9500 + name: manager + - containerPort: 9501 + name: conversion-wh + - containerPort: 9502 + name: admission-wh + - containerPort: 9503 + name: recov-backend + readinessProbe: + httpGet: + path: /v1/healthz + port: 9501 + scheme: HTTPS + volumeMounts: + - name: dev + mountPath: /host/dev/ + - name: proc + mountPath: /host/proc/ + - name: longhorn + mountPath: /var/lib/longhorn/ + mountPropagation: Bidirectional + - name: longhorn-grpc-tls + mountPath: /tls-files/ + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + volumes: + - name: dev + hostPath: + path: /dev/ + - name: proc + hostPath: + path: /proc/ + - name: longhorn + hostPath: + path: /var/lib/longhorn/ + - name: longhorn-grpc-tls + secret: + secretName: longhorn-grpc-tls + optional: true + {{- if .Values.privateRegistry.registrySecret }} + imagePullSecrets: + - name: {{ .Values.privateRegistry.registrySecret }} + {{- end }} + {{- if .Values.longhornManager.priorityClass }} + priorityClassName: {{ .Values.longhornManager.priorityClass | quote }} + {{- end }} + {{- if or .Values.longhornManager.tolerations .Values.global.cattle.windowsCluster.enabled }} + tolerations: + {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.tolerations }} +{{ toYaml .Values.global.cattle.windowsCluster.tolerations | indent 6 }} + {{- end }} + {{- if .Values.longhornManager.tolerations }} +{{ toYaml .Values.longhornManager.tolerations | indent 6 }} + {{- end }} + {{- end }} + {{- if or .Values.longhornManager.nodeSelector .Values.global.cattle.windowsCluster.enabled }} + nodeSelector: + {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.nodeSelector }} +{{ toYaml .Values.global.cattle.windowsCluster.nodeSelector | indent 8 }} + {{- end }} + {{- if .Values.longhornManager.nodeSelector }} +{{ toYaml .Values.longhornManager.nodeSelector | indent 8 }} + {{- end }} + {{- end }} + serviceAccountName: longhorn-service-account + updateStrategy: + rollingUpdate: + maxUnavailable: "100%" +--- +apiVersion: v1 +kind: Service +metadata: + labels: {{- include "longhorn.labels" . | nindent 4 }} + app: longhorn-manager + name: longhorn-backend + namespace: {{ include "release_namespace" . }} + {{- if .Values.longhornManager.serviceAnnotations }} + annotations: +{{ toYaml .Values.longhornManager.serviceAnnotations | indent 4 }} + {{- end }} +spec: + type: {{ .Values.service.manager.type }} + sessionAffinity: ClientIP + selector: + app: longhorn-manager + ports: + - name: manager + port: 9500 + targetPort: manager + {{- if .Values.service.manager.nodePort }} + nodePort: {{ .Values.service.manager.nodePort }} + {{- end }} diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/default-setting.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/default-setting.yaml new file mode 100644 index 0000000..ac38ba9 --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/default-setting.yaml @@ -0,0 +1,83 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: longhorn-default-setting + namespace: {{ include "release_namespace" . }} + labels: {{- include "longhorn.labels" . | nindent 4 }} +data: + default-setting.yaml: |- + {{ if not (kindIs "invalid" .Values.defaultSettings.backupTarget) }}backup-target: {{ .Values.defaultSettings.backupTarget }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.backupTargetCredentialSecret) }}backup-target-credential-secret: {{ .Values.defaultSettings.backupTargetCredentialSecret }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.allowRecurringJobWhileVolumeDetached) }}allow-recurring-job-while-volume-detached: {{ .Values.defaultSettings.allowRecurringJobWhileVolumeDetached }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.createDefaultDiskLabeledNodes) }}create-default-disk-labeled-nodes: {{ .Values.defaultSettings.createDefaultDiskLabeledNodes }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.defaultDataPath) }}default-data-path: {{ .Values.defaultSettings.defaultDataPath }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.replicaSoftAntiAffinity) }}replica-soft-anti-affinity: {{ .Values.defaultSettings.replicaSoftAntiAffinity }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.replicaAutoBalance) }}replica-auto-balance: {{ .Values.defaultSettings.replicaAutoBalance }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.storageOverProvisioningPercentage) }}storage-over-provisioning-percentage: {{ .Values.defaultSettings.storageOverProvisioningPercentage }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.storageMinimalAvailablePercentage) }}storage-minimal-available-percentage: {{ .Values.defaultSettings.storageMinimalAvailablePercentage }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.storageReservedPercentageForDefaultDisk) }}storage-reserved-percentage-for-default-disk: {{ .Values.defaultSettings.storageReservedPercentageForDefaultDisk }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.upgradeChecker) }}upgrade-checker: {{ .Values.defaultSettings.upgradeChecker }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.defaultReplicaCount) }}default-replica-count: {{ .Values.defaultSettings.defaultReplicaCount }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.defaultDataLocality) }}default-data-locality: {{ .Values.defaultSettings.defaultDataLocality }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.defaultLonghornStaticStorageClass) }}default-longhorn-static-storage-class: {{ .Values.defaultSettings.defaultLonghornStaticStorageClass }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.backupstorePollInterval) }}backupstore-poll-interval: {{ .Values.defaultSettings.backupstorePollInterval }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.failedBackupTTL) }}failed-backup-ttl: {{ .Values.defaultSettings.failedBackupTTL }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.restoreVolumeRecurringJobs) }}restore-volume-recurring-jobs: {{ .Values.defaultSettings.restoreVolumeRecurringJobs }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.recurringSuccessfulJobsHistoryLimit) }}recurring-successful-jobs-history-limit: {{ .Values.defaultSettings.recurringSuccessfulJobsHistoryLimit }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.recurringFailedJobsHistoryLimit) }}recurring-failed-jobs-history-limit: {{ .Values.defaultSettings.recurringFailedJobsHistoryLimit }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.supportBundleFailedHistoryLimit) }}support-bundle-failed-history-limit: {{ .Values.defaultSettings.supportBundleFailedHistoryLimit }}{{ end }} + {{- if or (not (kindIs "invalid" .Values.defaultSettings.taintToleration)) (.Values.global.cattle.windowsCluster.enabled) }} + taint-toleration: {{ $windowsDefaultSettingTaintToleration := list }}{{ $defaultSettingTaintToleration := list -}} + {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.defaultSetting.taintToleration -}} + {{- $windowsDefaultSettingTaintToleration = .Values.global.cattle.windowsCluster.defaultSetting.taintToleration -}} + {{- end -}} + {{- if not (kindIs "invalid" .Values.defaultSettings.taintToleration) -}} + {{- $defaultSettingTaintToleration = .Values.defaultSettings.taintToleration -}} + {{- end -}} + {{- $taintToleration := list $windowsDefaultSettingTaintToleration $defaultSettingTaintToleration }}{{ join ";" (compact $taintToleration) -}} + {{- end }} + {{- if or (not (kindIs "invalid" .Values.defaultSettings.systemManagedComponentsNodeSelector)) (.Values.global.cattle.windowsCluster.enabled) }} + system-managed-components-node-selector: {{ $windowsDefaultSettingNodeSelector := list }}{{ $defaultSettingNodeSelector := list -}} + {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.defaultSetting.systemManagedComponentsNodeSelector -}} + {{ $windowsDefaultSettingNodeSelector = .Values.global.cattle.windowsCluster.defaultSetting.systemManagedComponentsNodeSelector -}} + {{- end -}} + {{- if not (kindIs "invalid" .Values.defaultSettings.systemManagedComponentsNodeSelector) -}} + {{- $defaultSettingNodeSelector = .Values.defaultSettings.systemManagedComponentsNodeSelector -}} + {{- end -}} + {{- $nodeSelector := list $windowsDefaultSettingNodeSelector $defaultSettingNodeSelector }}{{ join ";" (compact $nodeSelector) -}} + {{- end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.priorityClass) }}priority-class: {{ .Values.defaultSettings.priorityClass }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.autoSalvage) }}auto-salvage: {{ .Values.defaultSettings.autoSalvage }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.autoDeletePodWhenVolumeDetachedUnexpectedly) }}auto-delete-pod-when-volume-detached-unexpectedly: {{ .Values.defaultSettings.autoDeletePodWhenVolumeDetachedUnexpectedly }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.disableSchedulingOnCordonedNode) }}disable-scheduling-on-cordoned-node: {{ .Values.defaultSettings.disableSchedulingOnCordonedNode }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.replicaZoneSoftAntiAffinity) }}replica-zone-soft-anti-affinity: {{ .Values.defaultSettings.replicaZoneSoftAntiAffinity }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.nodeDownPodDeletionPolicy) }}node-down-pod-deletion-policy: {{ .Values.defaultSettings.nodeDownPodDeletionPolicy }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.nodeDrainPolicy) }}node-drain-policy: {{ .Values.defaultSettings.nodeDrainPolicy }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.replicaReplenishmentWaitInterval) }}replica-replenishment-wait-interval: {{ .Values.defaultSettings.replicaReplenishmentWaitInterval }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.concurrentReplicaRebuildPerNodeLimit) }}concurrent-replica-rebuild-per-node-limit: {{ .Values.defaultSettings.concurrentReplicaRebuildPerNodeLimit }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.concurrentVolumeBackupRestorePerNodeLimit) }}concurrent-volume-backup-restore-per-node-limit: {{ .Values.defaultSettings.concurrentVolumeBackupRestorePerNodeLimit }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.disableRevisionCounter) }}disable-revision-counter: {{ .Values.defaultSettings.disableRevisionCounter }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.systemManagedPodsImagePullPolicy) }}system-managed-pods-image-pull-policy: {{ .Values.defaultSettings.systemManagedPodsImagePullPolicy }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.allowVolumeCreationWithDegradedAvailability) }}allow-volume-creation-with-degraded-availability: {{ .Values.defaultSettings.allowVolumeCreationWithDegradedAvailability }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.autoCleanupSystemGeneratedSnapshot) }}auto-cleanup-system-generated-snapshot: {{ .Values.defaultSettings.autoCleanupSystemGeneratedSnapshot }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.concurrentAutomaticEngineUpgradePerNodeLimit) }}concurrent-automatic-engine-upgrade-per-node-limit: {{ .Values.defaultSettings.concurrentAutomaticEngineUpgradePerNodeLimit }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.backingImageCleanupWaitInterval) }}backing-image-cleanup-wait-interval: {{ .Values.defaultSettings.backingImageCleanupWaitInterval }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.backingImageRecoveryWaitInterval) }}backing-image-recovery-wait-interval: {{ .Values.defaultSettings.backingImageRecoveryWaitInterval }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.guaranteedInstanceManagerCPU) }}guaranteed-instance-manager-cpu: {{ .Values.defaultSettings.guaranteedInstanceManagerCPU }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.kubernetesClusterAutoscalerEnabled) }}kubernetes-cluster-autoscaler-enabled: {{ .Values.defaultSettings.kubernetesClusterAutoscalerEnabled }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.orphanAutoDeletion) }}orphan-auto-deletion: {{ .Values.defaultSettings.orphanAutoDeletion }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.storageNetwork) }}storage-network: {{ .Values.defaultSettings.storageNetwork }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.deletingConfirmationFlag) }}deleting-confirmation-flag: {{ .Values.defaultSettings.deletingConfirmationFlag }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.engineReplicaTimeout) }}engine-replica-timeout: {{ .Values.defaultSettings.engineReplicaTimeout }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.snapshotDataIntegrity) }}snapshot-data-integrity: {{ .Values.defaultSettings.snapshotDataIntegrity }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.snapshotDataIntegrityImmediateCheckAfterSnapshotCreation) }}snapshot-data-integrity-immediate-check-after-snapshot-creation: {{ .Values.defaultSettings.snapshotDataIntegrityImmediateCheckAfterSnapshotCreation }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.snapshotDataIntegrityCronjob) }}snapshot-data-integrity-cronjob: {{ .Values.defaultSettings.snapshotDataIntegrityCronjob }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.removeSnapshotsDuringFilesystemTrim) }}remove-snapshots-during-filesystem-trim: {{ .Values.defaultSettings.removeSnapshotsDuringFilesystemTrim }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.fastReplicaRebuildEnabled) }}fast-replica-rebuild-enabled: {{ .Values.defaultSettings.fastReplicaRebuildEnabled }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.replicaFileSyncHttpClientTimeout) }}replica-file-sync-http-client-timeout: {{ .Values.defaultSettings.replicaFileSyncHttpClientTimeout }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.logLevel) }}log-level: {{ .Values.defaultSettings.logLevel }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.backupCompressionMethod) }}backup-compression-method: {{ .Values.defaultSettings.backupCompressionMethod }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.backupConcurrentLimit) }}backup-concurrent-limit: {{ .Values.defaultSettings.backupConcurrentLimit }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.restoreConcurrentLimit) }}restore-concurrent-limit: {{ .Values.defaultSettings.restoreConcurrentLimit }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.v2DataEngine) }}v2-data-engine: {{ .Values.defaultSettings.v2DataEngine }}{{ end }} + {{ if not (kindIs "invalid" .Values.defaultSettings.offlineReplicaRebuilding) }}offline-replica-rebuilding: {{ .Values.defaultSettings.offlineReplicaRebuilding }}{{ end }} diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/deployment-driver.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/deployment-driver.yaml new file mode 100644 index 0000000..f162fbf --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/deployment-driver.yaml @@ -0,0 +1,118 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: longhorn-driver-deployer + namespace: {{ include "release_namespace" . }} + labels: {{- include "longhorn.labels" . | nindent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + app: longhorn-driver-deployer + template: + metadata: + labels: {{- include "longhorn.labels" . | nindent 8 }} + app: longhorn-driver-deployer + spec: + initContainers: + - name: wait-longhorn-manager + image: {{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }} + command: ['sh', '-c', 'while [ $(curl -m 1 -s -o /dev/null -w "%{http_code}" http://longhorn-backend:9500/v1) != "200" ]; do echo waiting; sleep 2; done'] + containers: + - name: longhorn-driver-deployer + image: {{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - longhorn-manager + - -d + - deploy-driver + - --manager-image + - "{{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }}" + - --manager-url + - http://longhorn-backend:9500/v1 + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: SERVICE_ACCOUNT + valueFrom: + fieldRef: + fieldPath: spec.serviceAccountName + {{- if .Values.csi.kubeletRootDir }} + - name: KUBELET_ROOT_DIR + value: {{ .Values.csi.kubeletRootDir }} + {{- end }} + {{- if and .Values.image.csi.attacher.repository .Values.image.csi.attacher.tag }} + - name: CSI_ATTACHER_IMAGE + value: "{{ template "registry_url" . }}{{ .Values.image.csi.attacher.repository }}:{{ .Values.image.csi.attacher.tag }}" + {{- end }} + {{- if and .Values.image.csi.provisioner.repository .Values.image.csi.provisioner.tag }} + - name: CSI_PROVISIONER_IMAGE + value: "{{ template "registry_url" . }}{{ .Values.image.csi.provisioner.repository }}:{{ .Values.image.csi.provisioner.tag }}" + {{- end }} + {{- if and .Values.image.csi.nodeDriverRegistrar.repository .Values.image.csi.nodeDriverRegistrar.tag }} + - name: CSI_NODE_DRIVER_REGISTRAR_IMAGE + value: "{{ template "registry_url" . }}{{ .Values.image.csi.nodeDriverRegistrar.repository }}:{{ .Values.image.csi.nodeDriverRegistrar.tag }}" + {{- end }} + {{- if and .Values.image.csi.resizer.repository .Values.image.csi.resizer.tag }} + - name: CSI_RESIZER_IMAGE + value: "{{ template "registry_url" . }}{{ .Values.image.csi.resizer.repository }}:{{ .Values.image.csi.resizer.tag }}" + {{- end }} + {{- if and .Values.image.csi.snapshotter.repository .Values.image.csi.snapshotter.tag }} + - name: CSI_SNAPSHOTTER_IMAGE + value: "{{ template "registry_url" . }}{{ .Values.image.csi.snapshotter.repository }}:{{ .Values.image.csi.snapshotter.tag }}" + {{- end }} + {{- if and .Values.image.csi.livenessProbe.repository .Values.image.csi.livenessProbe.tag }} + - name: CSI_LIVENESS_PROBE_IMAGE + value: "{{ template "registry_url" . }}{{ .Values.image.csi.livenessProbe.repository }}:{{ .Values.image.csi.livenessProbe.tag }}" + {{- end }} + {{- if .Values.csi.attacherReplicaCount }} + - name: CSI_ATTACHER_REPLICA_COUNT + value: {{ .Values.csi.attacherReplicaCount | quote }} + {{- end }} + {{- if .Values.csi.provisionerReplicaCount }} + - name: CSI_PROVISIONER_REPLICA_COUNT + value: {{ .Values.csi.provisionerReplicaCount | quote }} + {{- end }} + {{- if .Values.csi.resizerReplicaCount }} + - name: CSI_RESIZER_REPLICA_COUNT + value: {{ .Values.csi.resizerReplicaCount | quote }} + {{- end }} + {{- if .Values.csi.snapshotterReplicaCount }} + - name: CSI_SNAPSHOTTER_REPLICA_COUNT + value: {{ .Values.csi.snapshotterReplicaCount | quote }} + {{- end }} + + {{- if .Values.privateRegistry.registrySecret }} + imagePullSecrets: + - name: {{ .Values.privateRegistry.registrySecret }} + {{- end }} + {{- if .Values.longhornDriver.priorityClass }} + priorityClassName: {{ .Values.longhornDriver.priorityClass | quote }} + {{- end }} + {{- if or .Values.longhornDriver.tolerations .Values.global.cattle.windowsCluster.enabled }} + tolerations: + {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.tolerations }} +{{ toYaml .Values.global.cattle.windowsCluster.tolerations | indent 6 }} + {{- end }} + {{- if .Values.longhornDriver.tolerations }} +{{ toYaml .Values.longhornDriver.tolerations | indent 6 }} + {{- end }} + {{- end }} + {{- if or .Values.longhornDriver.nodeSelector .Values.global.cattle.windowsCluster.enabled }} + nodeSelector: + {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.nodeSelector }} +{{ toYaml .Values.global.cattle.windowsCluster.nodeSelector | indent 8 }} + {{- end }} + {{- if .Values.longhornDriver.nodeSelector }} +{{ toYaml .Values.longhornDriver.nodeSelector | indent 8 }} + {{- end }} + {{- end }} + serviceAccountName: longhorn-service-account + securityContext: + runAsUser: 0 diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/deployment-ui.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/deployment-ui.yaml new file mode 100644 index 0000000..6bad5cd --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/deployment-ui.yaml @@ -0,0 +1,114 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: {{- include "longhorn.labels" . | nindent 4 }} + app: longhorn-ui + name: longhorn-ui + namespace: {{ include "release_namespace" . }} +spec: + replicas: {{ .Values.longhornUI.replicas }} + selector: + matchLabels: + app: longhorn-ui + template: + metadata: + labels: {{- include "longhorn.labels" . | nindent 8 }} + app: longhorn-ui + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 1 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app + operator: In + values: + - longhorn-ui + topologyKey: kubernetes.io/hostname + containers: + - name: longhorn-ui + image: {{ template "registry_url" . }}{{ .Values.image.longhorn.ui.repository }}:{{ .Values.image.longhorn.ui.tag }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + volumeMounts: + - name : nginx-cache + mountPath: /var/cache/nginx/ + - name : nginx-config + mountPath: /var/config/nginx/ + - name: var-run + mountPath: /var/run/ + ports: + - containerPort: 8000 + name: http + env: + - name: LONGHORN_MANAGER_IP + value: "http://longhorn-backend:9500" + - name: LONGHORN_UI_PORT + value: "8000" + volumes: + - emptyDir: {} + name: nginx-cache + - emptyDir: {} + name: nginx-config + - emptyDir: {} + name: var-run + {{- if .Values.privateRegistry.registrySecret }} + imagePullSecrets: + - name: {{ .Values.privateRegistry.registrySecret }} + {{- end }} + {{- if .Values.longhornUI.priorityClass }} + priorityClassName: {{ .Values.longhornUI.priorityClass | quote }} + {{- end }} + {{- if or .Values.longhornUI.tolerations .Values.global.cattle.windowsCluster.enabled }} + tolerations: + {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.tolerations }} +{{ toYaml .Values.global.cattle.windowsCluster.tolerations | indent 6 }} + {{- end }} + {{- if .Values.longhornUI.tolerations }} +{{ toYaml .Values.longhornUI.tolerations | indent 6 }} + {{- end }} + {{- end }} + {{- if or .Values.longhornUI.nodeSelector .Values.global.cattle.windowsCluster.enabled }} + nodeSelector: + {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.nodeSelector }} +{{ toYaml .Values.global.cattle.windowsCluster.nodeSelector | indent 8 }} + {{- end }} + {{- if .Values.longhornUI.nodeSelector }} +{{ toYaml .Values.longhornUI.nodeSelector | indent 8 }} + {{- end }} + {{- end }} +--- +kind: Service +apiVersion: v1 +metadata: + labels: {{- include "longhorn.labels" . | nindent 4 }} + app: longhorn-ui + {{- if eq .Values.service.ui.type "Rancher-Proxy" }} + kubernetes.io/cluster-service: "true" + {{- end }} + name: longhorn-frontend + namespace: {{ include "release_namespace" . }} +spec: + {{- if eq .Values.service.ui.type "Rancher-Proxy" }} + type: ClusterIP + {{- else }} + type: {{ .Values.service.ui.type }} + {{- end }} + {{- if and .Values.service.ui.loadBalancerIP (eq .Values.service.ui.type "LoadBalancer") }} + loadBalancerIP: {{ .Values.service.ui.loadBalancerIP }} + {{- end }} + {{- if and (eq .Values.service.ui.type "LoadBalancer") .Values.service.ui.loadBalancerSourceRanges }} + loadBalancerSourceRanges: {{- toYaml .Values.service.ui.loadBalancerSourceRanges | nindent 4 }} + {{- end }} + selector: + app: longhorn-ui + ports: + - name: http + port: 80 + targetPort: http + {{- if .Values.service.ui.nodePort }} + nodePort: {{ .Values.service.ui.nodePort }} + {{- else }} + nodePort: null + {{- end }} diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/ingress.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/ingress.yaml new file mode 100644 index 0000000..ee47f8b --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/ingress.yaml @@ -0,0 +1,48 @@ +{{- if .Values.ingress.enabled }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else -}} +apiVersion: networking.k8s.io/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: longhorn-ingress + namespace: {{ include "release_namespace" . }} + labels: {{- include "longhorn.labels" . | nindent 4 }} + app: longhorn-ingress + annotations: + {{- if .Values.ingress.secureBackends }} + ingress.kubernetes.io/secure-backends: "true" + {{- end }} + {{- range $key, $value := .Values.ingress.annotations }} + {{ $key }}: {{ $value | quote }} + {{- end }} +spec: + {{- if and .Values.ingress.ingressClassName (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.ingressClassName }} + {{- end }} + rules: + - host: {{ .Values.ingress.host }} + http: + paths: + - path: {{ default "" .Values.ingress.path }} + {{- if (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: ImplementationSpecific + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: longhorn-frontend + port: + number: 80 + {{- else }} + serviceName: longhorn-frontend + servicePort: 80 + {{- end }} +{{- if .Values.ingress.tls }} + tls: + - hosts: + - {{ .Values.ingress.host }} + secretName: {{ .Values.ingress.tlsSecret }} +{{- end }} +{{- end }} diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/backing-image-data-source-network-policy.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/backing-image-data-source-network-policy.yaml new file mode 100644 index 0000000..cc91054 --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/backing-image-data-source-network-policy.yaml @@ -0,0 +1,27 @@ +{{- if .Values.networkPolicies.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: backing-image-data-source + namespace: longhorn-system +spec: + podSelector: + matchLabels: + longhorn.io/component: backing-image-data-source + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app: longhorn-manager + - podSelector: + matchLabels: + longhorn.io/component: instance-manager + - podSelector: + matchLabels: + longhorn.io/component: backing-image-manager + - podSelector: + matchLabels: + longhorn.io/component: backing-image-data-source +{{- end }} diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/backing-image-manager-network-policy.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/backing-image-manager-network-policy.yaml new file mode 100644 index 0000000..ebc288f --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/backing-image-manager-network-policy.yaml @@ -0,0 +1,27 @@ +{{- if .Values.networkPolicies.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: backing-image-manager + namespace: longhorn-system +spec: + podSelector: + matchLabels: + longhorn.io/component: backing-image-manager + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app: longhorn-manager + - podSelector: + matchLabels: + longhorn.io/component: instance-manager + - podSelector: + matchLabels: + longhorn.io/component: backing-image-manager + - podSelector: + matchLabels: + longhorn.io/component: backing-image-data-source +{{- end }} diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/instance-manager-networking.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/instance-manager-networking.yaml new file mode 100644 index 0000000..6f03c6e --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/instance-manager-networking.yaml @@ -0,0 +1,27 @@ +{{- if .Values.networkPolicies.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: instance-manager + namespace: longhorn-system +spec: + podSelector: + matchLabels: + longhorn.io/component: instance-manager + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app: longhorn-manager + - podSelector: + matchLabels: + longhorn.io/component: instance-manager + - podSelector: + matchLabels: + longhorn.io/component: backing-image-manager + - podSelector: + matchLabels: + longhorn.io/component: backing-image-data-source +{{- end }} diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/manager-network-policy.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/manager-network-policy.yaml new file mode 100644 index 0000000..c9d763f --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/manager-network-policy.yaml @@ -0,0 +1,35 @@ +{{- if .Values.networkPolicies.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: longhorn-manager + namespace: longhorn-system +spec: + podSelector: + matchLabels: + app: longhorn-manager + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app: longhorn-manager + - podSelector: + matchLabels: + app: longhorn-ui + - podSelector: + matchLabels: + app: longhorn-csi-plugin + - podSelector: + matchLabels: + longhorn.io/managed-by: longhorn-manager + matchExpressions: + - { key: recurring-job.longhorn.io, operator: Exists } + - podSelector: + matchExpressions: + - { key: longhorn.io/job-task, operator: Exists } + - podSelector: + matchLabels: + app: longhorn-driver-deployer +{{- end }} diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/recovery-backend-network-policy.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/recovery-backend-network-policy.yaml new file mode 100644 index 0000000..cebe485 --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/recovery-backend-network-policy.yaml @@ -0,0 +1,17 @@ +{{- if .Values.networkPolicies.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: longhorn-recovery-backend + namespace: longhorn-system +spec: + podSelector: + matchLabels: + app: longhorn-manager + policyTypes: + - Ingress + ingress: + - ports: + - protocol: TCP + port: 9503 +{{- end }} diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/ui-frontend-network-policy.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/ui-frontend-network-policy.yaml new file mode 100644 index 0000000..04c8beb --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/ui-frontend-network-policy.yaml @@ -0,0 +1,46 @@ +{{- if and .Values.networkPolicies.enabled .Values.ingress.enabled (not (eq .Values.networkPolicies.type "")) }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: longhorn-ui-frontend + namespace: longhorn-system +spec: + podSelector: + matchLabels: + app: longhorn-ui + policyTypes: + - Ingress + ingress: + - from: + {{- if eq .Values.networkPolicies.type "rke1"}} + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: ingress-nginx + podSelector: + matchLabels: + app.kubernetes.io/component: controller + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx + {{- else if eq .Values.networkPolicies.type "rke2" }} + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + app.kubernetes.io/component: controller + app.kubernetes.io/instance: rke2-ingress-nginx + app.kubernetes.io/name: rke2-ingress-nginx + {{- else if eq .Values.networkPolicies.type "k3s" }} + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + app.kubernetes.io/name: traefik + ports: + - port: 8000 + protocol: TCP + - port: 80 + protocol: TCP + {{- end }} +{{- end }} diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/webhook-network-policy.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/webhook-network-policy.yaml new file mode 100644 index 0000000..c9790f6 --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/network-policies/webhook-network-policy.yaml @@ -0,0 +1,33 @@ +{{- if .Values.networkPolicies.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: longhorn-conversion-webhook + namespace: longhorn-system +spec: + podSelector: + matchLabels: + app: longhorn-manager + policyTypes: + - Ingress + ingress: + - ports: + - protocol: TCP + port: 9501 +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: longhorn-admission-webhook + namespace: longhorn-system +spec: + podSelector: + matchLabels: + app: longhorn-manager + policyTypes: + - Ingress + ingress: + - ports: + - protocol: TCP + port: 9502 +{{- end }} diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/postupgrade-job.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/postupgrade-job.yaml new file mode 100644 index 0000000..bb25a54 --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/postupgrade-job.yaml @@ -0,0 +1,56 @@ +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + "helm.sh/hook": post-upgrade + "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation + name: longhorn-post-upgrade + namespace: {{ include "release_namespace" . }} + labels: {{- include "longhorn.labels" . | nindent 4 }} +spec: + activeDeadlineSeconds: 900 + backoffLimit: 1 + template: + metadata: + name: longhorn-post-upgrade + labels: {{- include "longhorn.labels" . | nindent 8 }} + spec: + containers: + - name: longhorn-post-upgrade + image: {{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - longhorn-manager + - post-upgrade + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + restartPolicy: OnFailure + {{- if .Values.privateRegistry.registrySecret }} + imagePullSecrets: + - name: {{ .Values.privateRegistry.registrySecret }} + {{- end }} + {{- if .Values.longhornManager.priorityClass }} + priorityClassName: {{ .Values.longhornManager.priorityClass | quote }} + {{- end }} + serviceAccountName: longhorn-service-account + {{- if or .Values.longhornManager.tolerations .Values.global.cattle.windowsCluster.enabled }} + tolerations: + {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.tolerations }} +{{ toYaml .Values.global.cattle.windowsCluster.tolerations | indent 6 }} + {{- end }} + {{- if .Values.longhornManager.tolerations }} +{{ toYaml .Values.longhornManager.tolerations | indent 6 }} + {{- end }} + {{- end }} + {{- if or .Values.longhornManager.nodeSelector .Values.global.cattle.windowsCluster.enabled }} + nodeSelector: + {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.nodeSelector }} +{{ toYaml .Values.global.cattle.windowsCluster.nodeSelector | indent 8 }} + {{- end }} + {{- if .Values.longhornManager.nodeSelector }} +{{ toYaml .Values.longhornManager.nodeSelector | indent 8 }} + {{- end }} + {{- end }} diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/preupgrade-job.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/preupgrade-job.yaml new file mode 100644 index 0000000..357e6d7 --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/preupgrade-job.yaml @@ -0,0 +1,58 @@ +{{- if .Values.helmPreUpgradeCheckerJob.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + "helm.sh/hook": pre-upgrade + "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation,hook-failed + name: longhorn-pre-upgrade + namespace: {{ include "release_namespace" . }} + labels: {{- include "longhorn.labels" . | nindent 4 }} +spec: + activeDeadlineSeconds: 900 + backoffLimit: 1 + template: + metadata: + name: longhorn-pre-upgrade + labels: {{- include "longhorn.labels" . | nindent 8 }} + spec: + containers: + - name: longhorn-pre-upgrade + image: {{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - longhorn-manager + - pre-upgrade + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + restartPolicy: OnFailure + {{- if .Values.privateRegistry.registrySecret }} + imagePullSecrets: + - name: {{ .Values.privateRegistry.registrySecret }} + {{- end }} + {{- if .Values.longhornManager.priorityClass }} + priorityClassName: {{ .Values.longhornManager.priorityClass | quote }} + {{- end }} + serviceAccountName: longhorn-service-account + {{- if or .Values.longhornManager.tolerations .Values.global.cattle.windowsCluster.enabled }} + tolerations: + {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.tolerations }} +{{ toYaml .Values.global.cattle.windowsCluster.tolerations | indent 6 }} + {{- end }} + {{- if .Values.longhornManager.tolerations }} +{{ toYaml .Values.longhornManager.tolerations | indent 6 }} + {{- end }} + {{- end }} + {{- if or .Values.longhornManager.nodeSelector .Values.global.cattle.windowsCluster.enabled }} + nodeSelector: + {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.nodeSelector }} +{{ toYaml .Values.global.cattle.windowsCluster.nodeSelector | indent 8 }} + {{- end }} + {{- if .Values.longhornManager.nodeSelector }} +{{ toYaml .Values.longhornManager.nodeSelector | indent 8 }} + {{- end }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/psp.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/psp.yaml new file mode 100644 index 0000000..a2dfc05 --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/psp.yaml @@ -0,0 +1,66 @@ +{{- if .Values.enablePSP }} +apiVersion: policy/v1beta1 +kind: PodSecurityPolicy +metadata: + name: longhorn-psp + labels: {{- include "longhorn.labels" . | nindent 4 }} +spec: + privileged: true + allowPrivilegeEscalation: true + requiredDropCapabilities: + - NET_RAW + allowedCapabilities: + - SYS_ADMIN + hostNetwork: false + hostIPC: false + hostPID: true + runAsUser: + rule: RunAsAny + seLinux: + rule: RunAsAny + fsGroup: + rule: RunAsAny + supplementalGroups: + rule: RunAsAny + volumes: + - configMap + - downwardAPI + - emptyDir + - secret + - projected + - hostPath +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: longhorn-psp-role + labels: {{- include "longhorn.labels" . | nindent 4 }} + namespace: {{ include "release_namespace" . }} +rules: +- apiGroups: + - policy + resources: + - podsecuritypolicies + verbs: + - use + resourceNames: + - longhorn-psp +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: longhorn-psp-binding + labels: {{- include "longhorn.labels" . | nindent 4 }} + namespace: {{ include "release_namespace" . }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: longhorn-psp-role +subjects: +- kind: ServiceAccount + name: longhorn-service-account + namespace: {{ include "release_namespace" . }} +- kind: ServiceAccount + name: default + namespace: {{ include "release_namespace" . }} +{{- end }} diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/registry-secret.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/registry-secret.yaml new file mode 100644 index 0000000..3c6b1dc --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/registry-secret.yaml @@ -0,0 +1,13 @@ +{{- if .Values.privateRegistry.createSecret }} +{{- if .Values.privateRegistry.registrySecret }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.privateRegistry.registrySecret }} + namespace: {{ include "release_namespace" . }} + labels: {{- include "longhorn.labels" . | nindent 4 }} +type: kubernetes.io/dockerconfigjson +data: + .dockerconfigjson: {{ template "secret" . }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/serviceaccount.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/serviceaccount.yaml new file mode 100644 index 0000000..a563d68 --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/serviceaccount.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: longhorn-service-account + namespace: {{ include "release_namespace" . }} + labels: {{- include "longhorn.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: longhorn-support-bundle + namespace: {{ include "release_namespace" . }} + labels: {{- include "longhorn.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} \ No newline at end of file diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/services.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/services.yaml new file mode 100644 index 0000000..7da9d18 --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/services.yaml @@ -0,0 +1,74 @@ +apiVersion: v1 +kind: Service +metadata: + labels: {{- include "longhorn.labels" . | nindent 4 }} + app: longhorn-conversion-webhook + name: longhorn-conversion-webhook + namespace: {{ include "release_namespace" . }} +spec: + type: ClusterIP + sessionAffinity: ClientIP + selector: + app: longhorn-manager + ports: + - name: conversion-webhook + port: 9501 + targetPort: conversion-wh +--- +apiVersion: v1 +kind: Service +metadata: + labels: {{- include "longhorn.labels" . | nindent 4 }} + app: longhorn-admission-webhook + name: longhorn-admission-webhook + namespace: {{ include "release_namespace" . }} +spec: + type: ClusterIP + sessionAffinity: ClientIP + selector: + app: longhorn-manager + ports: + - name: admission-webhook + port: 9502 + targetPort: admission-wh +--- +apiVersion: v1 +kind: Service +metadata: + labels: {{- include "longhorn.labels" . | nindent 4 }} + app: longhorn-recovery-backend + name: longhorn-recovery-backend + namespace: {{ include "release_namespace" . }} +spec: + type: ClusterIP + sessionAffinity: ClientIP + selector: + app: longhorn-manager + ports: + - name: recovery-backend + port: 9503 + targetPort: recov-backend +--- +apiVersion: v1 +kind: Service +metadata: + labels: {{- include "longhorn.labels" . | nindent 4 }} + name: longhorn-engine-manager + namespace: {{ include "release_namespace" . }} +spec: + clusterIP: None + selector: + longhorn.io/component: instance-manager + longhorn.io/instance-manager-type: engine +--- +apiVersion: v1 +kind: Service +metadata: + labels: {{- include "longhorn.labels" . | nindent 4 }} + name: longhorn-replica-manager + namespace: {{ include "release_namespace" . }} +spec: + clusterIP: None + selector: + longhorn.io/component: instance-manager + longhorn.io/instance-manager-type: replica diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/storageclass.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/storageclass.yaml new file mode 100644 index 0000000..6832517 --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/storageclass.yaml @@ -0,0 +1,44 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: longhorn-storageclass + namespace: {{ include "release_namespace" . }} + labels: {{- include "longhorn.labels" . | nindent 4 }} +data: + storageclass.yaml: | + kind: StorageClass + apiVersion: storage.k8s.io/v1 + metadata: + name: longhorn + annotations: + storageclass.kubernetes.io/is-default-class: {{ .Values.persistence.defaultClass | quote }} + provisioner: driver.longhorn.io + allowVolumeExpansion: true + reclaimPolicy: "{{ .Values.persistence.reclaimPolicy }}" + volumeBindingMode: Immediate + parameters: + numberOfReplicas: "{{ .Values.persistence.defaultClassReplicaCount }}" + staleReplicaTimeout: "30" + fromBackup: "" + {{- if .Values.persistence.defaultFsType }} + fsType: "{{ .Values.persistence.defaultFsType }}" + {{- end }} + {{- if .Values.persistence.defaultMkfsParams }} + mkfsParams: "{{ .Values.persistence.defaultMkfsParams }}" + {{- end }} + {{- if .Values.persistence.migratable }} + migratable: "{{ .Values.persistence.migratable }}" + {{- end }} + {{- if .Values.persistence.backingImage.enable }} + backingImage: {{ .Values.persistence.backingImage.name }} + backingImageDataSourceType: {{ .Values.persistence.backingImage.dataSourceType }} + backingImageDataSourceParameters: {{ .Values.persistence.backingImage.dataSourceParameters }} + backingImageChecksum: {{ .Values.persistence.backingImage.expectedChecksum }} + {{- end }} + {{- if .Values.persistence.recurringJobSelector.enable }} + recurringJobSelector: '{{ .Values.persistence.recurringJobSelector.jobList }}' + {{- end }} + dataLocality: {{ .Values.persistence.defaultDataLocality | quote }} + {{- if .Values.persistence.defaultNodeSelector.enable }} + nodeSelector: "{{ .Values.persistence.defaultNodeSelector.selector }}" + {{- end }} diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/tls-secrets.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/tls-secrets.yaml new file mode 100644 index 0000000..74c4342 --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/tls-secrets.yaml @@ -0,0 +1,16 @@ +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.secrets }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .name }} + namespace: {{ include "release_namespace" $ }} + labels: {{- include "longhorn.labels" $ | nindent 4 }} + app: longhorn +type: kubernetes.io/tls +data: + tls.crt: {{ .certificate | b64enc }} + tls.key: {{ .key | b64enc }} +--- +{{- end }} +{{- end }} diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/uninstall-job.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/uninstall-job.yaml new file mode 100644 index 0000000..968f420 --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/uninstall-job.yaml @@ -0,0 +1,57 @@ +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + "helm.sh/hook": pre-delete + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + name: longhorn-uninstall + namespace: {{ include "release_namespace" . }} + labels: {{- include "longhorn.labels" . | nindent 4 }} +spec: + activeDeadlineSeconds: 900 + backoffLimit: 1 + template: + metadata: + name: longhorn-uninstall + labels: {{- include "longhorn.labels" . | nindent 8 }} + spec: + containers: + - name: longhorn-uninstall + image: {{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - longhorn-manager + - uninstall + - --force + env: + - name: LONGHORN_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + restartPolicy: Never + {{- if .Values.privateRegistry.registrySecret }} + imagePullSecrets: + - name: {{ .Values.privateRegistry.registrySecret }} + {{- end }} + {{- if .Values.longhornManager.priorityClass }} + priorityClassName: {{ .Values.longhornManager.priorityClass | quote }} + {{- end }} + serviceAccountName: longhorn-service-account + {{- if or .Values.longhornManager.tolerations .Values.global.cattle.windowsCluster.enabled }} + tolerations: + {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.tolerations }} +{{ toYaml .Values.global.cattle.windowsCluster.tolerations | indent 6 }} + {{- end }} + {{- if .Values.longhornManager.tolerations }} +{{ toYaml .Values.longhornManager.tolerations | indent 6 }} + {{- end }} + {{- end }} + {{- if or .Values.longhornManager.nodeSelector .Values.global.cattle.windowsCluster.enabled }} + nodeSelector: + {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.nodeSelector }} +{{ toYaml .Values.global.cattle.windowsCluster.nodeSelector | indent 8 }} + {{- end }} + {{- if or .Values.longhornManager.nodeSelector }} +{{ toYaml .Values.longhornManager.nodeSelector | indent 8 }} + {{- end }} + {{- end }} diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/validate-psp-install.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/validate-psp-install.yaml new file mode 100644 index 0000000..0df98e3 --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/templates/validate-psp-install.yaml @@ -0,0 +1,7 @@ +#{{- if gt (len (lookup "rbac.authorization.k8s.io/v1" "ClusterRole" "" "")) 0 -}} +#{{- if .Values.enablePSP }} +#{{- if not (.Capabilities.APIVersions.Has "policy/v1beta1/PodSecurityPolicy") }} +#{{- fail "The target cluster does not have the PodSecurityPolicy API resource. Please disable PSPs in this chart before proceeding." -}} +#{{- end }} +#{{- end }} +#{{- end }} \ No newline at end of file diff --git a/longhorn-system/charts/longhorn-v1.5.3/longhorn/values.yaml b/longhorn-system/charts/longhorn-v1.5.3/longhorn/values.yaml new file mode 100644 index 0000000..65878ff --- /dev/null +++ b/longhorn-system/charts/longhorn-v1.5.3/longhorn/values.yaml @@ -0,0 +1,296 @@ +# Default values for longhorn. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +global: + cattle: + systemDefaultRegistry: "" + windowsCluster: + # Enable this to allow Longhorn to run on the Rancher deployed Windows cluster + enabled: false + # Tolerate Linux node taint + tolerations: + - key: "cattle.io/os" + value: "linux" + effect: "NoSchedule" + operator: "Equal" + # Select Linux nodes + nodeSelector: + kubernetes.io/os: "linux" + # Recognize toleration and node selector for Longhorn run-time created components + defaultSetting: + taintToleration: cattle.io/os=linux:NoSchedule + systemManagedComponentsNodeSelector: kubernetes.io/os:linux + +networkPolicies: + enabled: false + # Available types: k3s, rke2, rke1 + type: "k3s" + +image: + longhorn: + engine: + repository: longhornio/longhorn-engine + tag: v1.5.3 + manager: + repository: longhornio/longhorn-manager + tag: v1.5.3 + ui: + repository: longhornio/longhorn-ui + tag: v1.5.3 + instanceManager: + repository: longhornio/longhorn-instance-manager + tag: v1.5.3 + shareManager: + repository: longhornio/longhorn-share-manager + tag: v1.5.3 + backingImageManager: + repository: longhornio/backing-image-manager + tag: v1.5.3 + supportBundleKit: + repository: longhornio/support-bundle-kit + tag: v0.0.27 + csi: + attacher: + repository: longhornio/csi-attacher + tag: v4.2.0 + provisioner: + repository: longhornio/csi-provisioner + tag: v3.4.1 + nodeDriverRegistrar: + repository: longhornio/csi-node-driver-registrar + tag: v2.7.0 + resizer: + repository: longhornio/csi-resizer + tag: v1.7.0 + snapshotter: + repository: longhornio/csi-snapshotter + tag: v6.2.1 + livenessProbe: + repository: longhornio/livenessprobe + tag: v2.9.0 + pullPolicy: IfNotPresent + +service: + ui: + type: ClusterIP + nodePort: null + manager: + type: ClusterIP + nodePort: "" + loadBalancerIP: "" + loadBalancerSourceRanges: "" + +persistence: + defaultClass: true + defaultFsType: ext4 + defaultMkfsParams: "" + defaultClassReplicaCount: 3 + defaultDataLocality: disabled # best-effort otherwise + reclaimPolicy: Delete + migratable: false + recurringJobSelector: + enable: false + jobList: [] + backingImage: + enable: false + name: ~ + dataSourceType: ~ + dataSourceParameters: ~ + expectedChecksum: ~ + defaultNodeSelector: + enable: false # disable by default + selector: "" + removeSnapshotsDuringFilesystemTrim: ignored # "enabled" or "disabled" otherwise + +helmPreUpgradeCheckerJob: + enabled: true + +csi: + kubeletRootDir: ~ + attacherReplicaCount: ~ + provisionerReplicaCount: ~ + resizerReplicaCount: ~ + snapshotterReplicaCount: ~ + +defaultSettings: + backupTarget: ~ + backupTargetCredentialSecret: ~ + allowRecurringJobWhileVolumeDetached: ~ + createDefaultDiskLabeledNodes: ~ + defaultDataPath: ~ + defaultDataLocality: ~ + replicaSoftAntiAffinity: ~ + replicaAutoBalance: ~ + storageOverProvisioningPercentage: ~ + storageMinimalAvailablePercentage: ~ + storageReservedPercentageForDefaultDisk: ~ + upgradeChecker: ~ + defaultReplicaCount: ~ + defaultLonghornStaticStorageClass: ~ + backupstorePollInterval: ~ + failedBackupTTL: ~ + restoreVolumeRecurringJobs: ~ + recurringSuccessfulJobsHistoryLimit: ~ + recurringFailedJobsHistoryLimit: ~ + supportBundleFailedHistoryLimit: ~ + taintToleration: ~ + systemManagedComponentsNodeSelector: ~ + priorityClass: ~ + autoSalvage: ~ + autoDeletePodWhenVolumeDetachedUnexpectedly: ~ + disableSchedulingOnCordonedNode: ~ + replicaZoneSoftAntiAffinity: ~ + nodeDownPodDeletionPolicy: ~ + nodeDrainPolicy: ~ + replicaReplenishmentWaitInterval: ~ + concurrentReplicaRebuildPerNodeLimit: ~ + concurrentVolumeBackupRestorePerNodeLimit: ~ + disableRevisionCounter: ~ + systemManagedPodsImagePullPolicy: ~ + allowVolumeCreationWithDegradedAvailability: ~ + autoCleanupSystemGeneratedSnapshot: ~ + concurrentAutomaticEngineUpgradePerNodeLimit: ~ + backingImageCleanupWaitInterval: ~ + backingImageRecoveryWaitInterval: ~ + guaranteedInstanceManagerCPU: ~ + kubernetesClusterAutoscalerEnabled: ~ + orphanAutoDeletion: ~ + storageNetwork: ~ + deletingConfirmationFlag: ~ + engineReplicaTimeout: ~ + snapshotDataIntegrity: ~ + snapshotDataIntegrityImmediateCheckAfterSnapshotCreation: ~ + snapshotDataIntegrityCronjob: ~ + removeSnapshotsDuringFilesystemTrim: ~ + fastReplicaRebuildEnabled: ~ + replicaFileSyncHttpClientTimeout: ~ + logLevel: ~ + backupCompressionMethod: ~ + backupConcurrentLimit: ~ + restoreConcurrentLimit: ~ + v2DataEngine: ~ + offlineReplicaRebuilding: ~ +privateRegistry: + createSecret: ~ + registryUrl: ~ + registryUser: ~ + registryPasswd: ~ + registrySecret: ~ + +longhornManager: + log: + ## Allowed values are `plain` or `json`. + format: plain + priorityClass: ~ + tolerations: [] + ## If you want to set tolerations for Longhorn Manager DaemonSet, delete the `[]` in the line above + ## and uncomment this example block + # - key: "key" + # operator: "Equal" + # value: "value" + # effect: "NoSchedule" + nodeSelector: {} + ## If you want to set node selector for Longhorn Manager DaemonSet, delete the `{}` in the line above + ## and uncomment this example block + # label-key1: "label-value1" + # label-key2: "label-value2" + serviceAnnotations: {} + ## If you want to set annotations for the Longhorn Manager service, delete the `{}` in the line above + ## and uncomment this example block + # annotation-key1: "annotation-value1" + # annotation-key2: "annotation-value2" + +longhornDriver: + priorityClass: ~ + tolerations: [] + ## If you want to set tolerations for Longhorn Driver Deployer Deployment, delete the `[]` in the line above + ## and uncomment this example block + # - key: "key" + # operator: "Equal" + # value: "value" + # effect: "NoSchedule" + nodeSelector: {} + ## If you want to set node selector for Longhorn Driver Deployer Deployment, delete the `{}` in the line above + ## and uncomment this example block + # label-key1: "label-value1" + # label-key2: "label-value2" + +longhornUI: + replicas: 2 + priorityClass: ~ + tolerations: [] + ## If you want to set tolerations for Longhorn UI Deployment, delete the `[]` in the line above + ## and uncomment this example block + # - key: "key" + # operator: "Equal" + # value: "value" + # effect: "NoSchedule" + nodeSelector: {} + ## If you want to set node selector for Longhorn UI Deployment, delete the `{}` in the line above + ## and uncomment this example block + # label-key1: "label-value1" + # label-key2: "label-value2" + +ingress: + ## Set to true to enable ingress record generation + enabled: false + + ## Add ingressClassName to the Ingress + ## Can replace the kubernetes.io/ingress.class annotation on v1.18+ + ingressClassName: ~ + + host: sslip.io + + ## Set this to true in order to enable TLS on the ingress record + tls: false + + ## Enable this in order to enable that the backend service will be connected at port 443 + secureBackends: false + + ## If TLS is set to true, you must declare what secret will store the key/certificate for TLS + tlsSecret: longhorn.local-tls + + ## If ingress is enabled you can set the default ingress path + ## then you can access the UI by using the following full path {{host}}+{{path}} + path: / + + ## Ingress annotations done as key:value pairs + ## If you're using kube-lego, you will want to add: + ## kubernetes.io/tls-acme: true + ## + ## For a full list of possible ingress annotations, please see + ## ref: https://github.com/kubernetes/ingress-nginx/blob/master/docs/annotations.md + ## + ## If tls is set to true, annotation ingress.kubernetes.io/secure-backends: "true" will automatically be set + annotations: + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: true + + secrets: + ## If you're providing your own certificates, please use this to add the certificates as secrets + ## key and certificate should start with -----BEGIN CERTIFICATE----- or + ## -----BEGIN RSA PRIVATE KEY----- + ## + ## name should line up with a tlsSecret set further up + ## If you're using kube-lego, this is unneeded, as it will create the secret for you if it is not set + ## + ## It is also possible to create and manage the certificates outside of this helm chart + ## Please see README.md for more information + # - name: longhorn.local-tls + # key: + # certificate: + +# For Kubernetes < v1.25, if your cluster enables Pod Security Policy admission controller, +# set this to `true` to ship longhorn-psp which allow privileged Longhorn pods to start +enablePSP: false + +## Specify override namespace, specifically this is useful for using longhorn as sub-chart +## and its release namespace is not the `longhorn-system` +namespaceOverride: "" + +# Annotations to add to the Longhorn Manager DaemonSet Pods. Optional. +annotations: {} + +serviceAccount: + # Annotations to add to the service account + annotations: {} diff --git a/longhorn-system/kustomization.yaml b/longhorn-system/kustomization.yaml new file mode 100644 index 0000000..4786173 --- /dev/null +++ b/longhorn-system/kustomization.yaml @@ -0,0 +1,24 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: longhorn-system + +resources: + - longhorn-ingress.yaml + +helmCharts: + - name: longhorn + repo: https://charts.longhorn.io + version: v1.5.3 # Pin your version for stability + releaseName: longhorn + namespace: longhorn-system + valuesFile: values.yaml + +patches: + - target: + kind: StorageClass + name: longhorn + patch: |- + - op: add + path: /metadata/annotations/storageclass.kubernetes.io~1is-default-class + value: "true" \ No newline at end of file diff --git a/longhorn-system/longhorn-ingress.yaml b/longhorn-system/longhorn-ingress.yaml new file mode 100644 index 0000000..24e5b28 --- /dev/null +++ b/longhorn-system/longhorn-ingress.yaml @@ -0,0 +1,19 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: longhorn-ingress + namespace: longhorn-system +spec: + rules: + - host: longhorn.local + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: longhorn-frontend + port: + number: 80 + +# kubectl apply -f system/longhorn-ingress.yaml \ No newline at end of file diff --git a/longhorn-system/values.yaml b/longhorn-system/values.yaml new file mode 100644 index 0000000..2f28c48 --- /dev/null +++ b/longhorn-system/values.yaml @@ -0,0 +1,17 @@ +persistence: + defaultClass: true + defaultClassReplicaCount: 3 + +# Disable so that longhorn can run on dbworker and nworker +# nodeSelector: +# node-role.kubernetes.io/nworker: "true" + +# This ensures the storage UI and manager also stay on those nodes +# longhornManager: +# nodeSelector: +# node-role.kubernetes.io/nworker: "true" + +# Installing Longhorn on the control plane (VM1) without proper configuration usually causes this "stuck" state +# longhornManager: +# nodeSelector: +# node-role.kubernetes.io/control-plane: "false" # Explicitly avoid VM1 \ No newline at end of file diff --git a/manuals.md b/manuals.md new file mode 100644 index 0000000..533cda0 --- /dev/null +++ b/manuals.md @@ -0,0 +1,271 @@ +### Getting Started + +#### 1. Using ansible +``` +a. ansible-playbook -i hosts.ini ansible-prepare-k3s.yml +if not working: +export ANSIBLE_CONFIG=./ansible.cfg +sed -i 's/\r$//' hosts.ini + +b. copy content of file from control-plane (sudo cat /etc/rancher/k3s/k3s.yaml) to config inside C:\Users\wendg2\.kube +c. kustomize build --enable-helm . | kubectl apply --server-side --force-conflicts -f - +d. ansible-playbook -i hosts.ini deploy-k3s.yml +``` +#### 2. Install kustomize +- Download version > v4.x fromhttps://github.com/kubernetes-sigs/kustomize/releases and set the path in your environment variables +- Test kustomize + + +#### 3. Setup k3s remote control +export ANSIBLE_CONFIG=./ansible.cfg +ssh-keygen -R 192.168.3.91 # in PS +ssh-keygen -f '/home/josh/.ssh/known_hosts' -R '192.168.3.91' # in wsl + +#### How Ansible finds its Config (The Hierarchy) +Ansible looks for its configuration in a very specific order. It stops at the first one it finds: +``` +1. ANSIBLE_CONFIG (The environment variable): This is the "Nuclear Option." It overrides everything else. + +2. ansible.cfg (In the current directory): This is what you were trying to use, but it was being ignored due to the permissions. + +3. ~/.ansible.cfg (In your home directory): Personal user settings. + +4. /etc/ansible/ansible.cfg: The global system default +``` +#### 4. using kustomize and helm together +- For yugabyte there must be enough space +kustomize build . --enable-helm | kubectl apply -f - +kustomize build . | kubectl apply --server-side --force-conflicts -f - # from root folder +kustomize build . | kubectl apply --server-side --force-conflicts -f - # from some folder + +for longhorn-system, argocd, linkerd, etc + +- For postgres we use cnpg operator + ``` + We must run twice: + kustomize build . --enable-helm | kubectl apply -f - + kubectl get pods -n cnpg-system + kubectl get cluster -n db -w + kubectl get pods -n db + ``` +#### 5.Start our apps +``` +a. kubectl port-forward svc/cosmo-router 3002:3002 -n apps +b. kubectl run nats-box-temp --image=natsio/nats-box -n infra --rm -it -- nats -s nats://nats-cluster:4222 sub "input_request_logs" +c. kubectl exec -it nats-box nats -n infra -- /bin/sh + +nats -s nats://nats-cluster.infra.svc.cluster.local:4222 stream add request_stream --subjects "input_request_logs" --ack --storage file --retention limits --max-msgs=-1 --max-bytes=-1 - +-max-age=1y --replicas 3 + +FQDN with pattern as `[service-name].[namespace].svc.cluster.local` +- find service name with : kubectl get svc -n db +- find service name is to be found through labels : +service-name for nats-cluster from app.kubernetes.io/instance +nats-cluster in labels app.kubernetes.io/instance=nats-cluster +d. kubectl logs -f benthos-6ff4b9dfb5-7klqr -n infra +``` + +#### Apply Changes + +- After modifying Benthos ( directly from benthos folder ): + ``` + kustomize build . | kubectl apply -f - or + kubectl apply -f benthos-deployment.yaml + ``` +- After modifying cosmo router: helm upgrade cosmo-router . -n apps + + + +#### Find secret +kubectl exec -it deployment/benthos -n infra -- env | findstr ECOM_PASS +kubectl get secret postgres-ha-app -n infra +kubectl get secrets -n infra +kubectl get secret postgres-ha-app -n db -o jsonpath='{.data}' + + +#### Create secret + +#### Force-copy secrets +1. This copies the secret so the 'infra' namespace can see it +kubectl get secret postgres-ha-app -n db -o yaml | %{ $_ -replace "namespace: db", "namespace: infra" } | kubectl apply -f - +2. force-copy from db to infra and clean up the metadata so Kubernetes accepts them as "new" secrets in the infrastructure namespace. +``` +Method 2a.Copy the app secret to the infra namespace +$secret = kubectl get secret postgres-ha-app -n db -o json | ConvertFrom-Json +$secret.metadata.psobject.Properties.Remove('namespace') +$secret.metadata.psobject.Properties.Remove('resourceVersion') +$secret.metadata.psobject.Properties.Remove('uid') +$secret | ConvertTo-Json | kubectl apply -n infra -f - + + +Method 2b. Copy the app secret from 'db' to 'infra' +kubectl get secret postgres-ha-app --namespace=db -o yaml | ` + ForEach-Object { $_ -replace 'namespace: db', 'namespace: infra' } | ` + kubectl apply -f - + +# Copy the superuser secret from 'db' to 'infra' +kubectl get secret postgres-ha-superuser --namespace=db -o yaml | ` + ForEach-Object { $_ -replace 'namespace: db', 'namespace: infra' } | ` + kubectl apply -f - + +or + +Method 2c.Export from DB namespace, strip IDs, and apply to INFRA namespace +kubectl get secret postgres-ha-app -n db -o yaml | ` + Select-String -Pattern "ownerReferences|resourceVersion|uid|creationTimestamp|namespace: db" -NotMatch | ` + Out-String | ForEach-Object { $_ + "`n namespace: infra" } | kubectl apply -f - + +kubectl get secret postgres-ha-superuser -n db -o yaml | ` + Select-String -Pattern "ownerReferences|resourceVersion|uid|creationTimestamp|namespace: db" -NotMatch | ` + Out-String | ForEach-Object { $_ + "`n namespace: infra" } | kubectl apply -f - + +or +Mehod 2d. Copy the app secret +kubectl get secret postgres-ha-app -n db -o json | jq 'del(.metadata.namespace,.metadata.resourceVersion,.metadata.uid)' | kubectl apply -n infra -f - + +# Copy the superuser secret +kubectl get secret postgres-ha-superuser -n db -o json | jq 'del(.metadata.namespace,.metadata.resourceVersion,.metadata.uid)' | kubectl apply -n infra -f - + +``` +#### Debug +``` +kubectl get all -A +kubectl describe pod yb-master-0 -n db +kubectl cluster-info +kubectl get cluster -n db -w # cnpg operator +kubectl get nodes.longhorn.io -n longhorn-system +kubectl get pods -n longhorn-system -o wide +kubectl get svc -n db +kubectl logs -f deployment/benthos -n infra --all-containers +NATS: +kubectl exec -it nats-box-temp nats -n infra -- /bin/sh + nats -s nats://nats-cluster:4222 sub "output_request_logs" +``` +#### Delete +Mostly for db use all and -l label +kubectl get pods -n db --show-labels +``` +From the related directory: kustomize build . | kubectl delete -f - +kubectl delete all -l app=yb-master -n db +kubectl delete all -l app=yb-tserver -n db +kubectl delete all -l cnpg.io/cluster=postgres-ha -n db +For shrinking volumes in postgres: +kubectl delete cluster postgres-ha -n db +kubectl delete pvc --all -n db +``` +#### Known error/Bugs +``` +1. In wsl Running helm command or kubectl + +Error: Kubernetes cluster unreachable: Get "http://localhost:8080/version": dial tcp 127.0.0.1:8080: +a. Create the directory if it doesn't exist +mkdir -p ~/.kube + +b. Link your Windows kubeconfig to your WSL home directory +ln -s /mnt/c/Users//.kube/config ~/.kube/config + +This fix got executed in deploy-k3s + +2. with httproutes.gateway.networking.k8s.io +kubectl delete crd httproutes.gateway.networking.k8s.io +kubectl edit crd httproutes.gateway.networking.k8s.io (add v1 back into the spec.versions list) +kubectl get crd | grep gateway.networking.k8s.io + +3. pods exist in wsl but not in PS + +sudo chmod 644 /etc/rancher/k3s/k3s.yaml +Run in PS +kustomize build . --enable-helm | kubectl apply --server-side --force-conflicts -f - + +4. pods exist in PS but not in wsl + +mkdir -p ~/.kube +sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config +sudo chown $(id -u):$(id -g) ~/.kube/config +export KUBECONFIG=~/.kube/config +``` + + +#### Login to db +``` +Getting user name +$userBase64 = kubectl get secret postgres-ha-app -n db -o jsonpath='{.data.username}' +[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($userBase64)) + +Getting password for user invixel_admin: +For Powershell +- $pass = kubectl get secret postgres-ha-app -n db -o jsonpath='{.data.password}' +- [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($pass)) + +Ours: +0lkzPxlwj6JVOXwwoLYROZJsONJoPK3MtrqkxnH3iaXUs0gFg0WL78RxyDdB86Sk +Getting password for super user +$pass = kubectl get secret postgres-ha-superuser -n db -o jsonpath='{.data.password}' +[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($pass)) +Ours: +CuUwr9dPXJibVFygh7oGastvIJb4syMZXKljsf0dbEl91TwuYLqvEW35hN98ytKe + +1. From inside the cluster (Quick Check) +kubectl exec -it postgres-ha-1 -n db -- psql -U postgres + +2. From windows machine: +kubectl port-forward svc/postgres-ha-rw -n db 5432:5432 +Open your DB Tool and connect to: + +Host: localhost + +Port: 5432 +User: postgres +Password: (The one you decoded in step 2) +``` + +#### Primary Key & Sequential +SELECT increment_by, cache_size, last_value +FROM pg_sequences +WHERE schemaname = 'public' AND sequencename = 'request_logs_id_seq'; + +ALTER SEQUENCE request_logs_id_seq INCREMENT BY 1; + + +#### Alternative using ansible +--- +- name: Deploy Longhorn to k3s Cluster + hosts: master + become: yes + vars: + # Path to k3s config on the master node + kubeconfig: /etc/rancher/k3s/k3s.yaml + longhorn_namespace: longhorn-system + + tasks: + - name: Add Longhorn Helm repo + kubernetes.core.helm_repository: + name: longhorn + repo_url: "https://charts.longhorn.io" + + - name: Create Longhorn namespace + kubernetes.core.k8s: + name: "{{ longhorn_namespace }}" + kind: Namespace + state: present + kubeconfig: "{{ kubeconfig }}" + + - name: Install Longhorn via Helm + kubernetes.core.helm: + name: longhorn + chart_ref: longhorn/longhorn + release_namespace: "{{ longhorn_namespace }}" + kubeconfig: "{{ kubeconfig }}" + wait: yes + # Longhorn requires open-iscsi and nfs-client on nodes + # Ensure those are installed on your workers first! + + - name: Set Longhorn as default StorageClass + kubernetes.core.k8s_json_patch: + kind: StorageClass + name: longhorn + kubeconfig: "{{ kubeconfig }}" + patch: + - op: add + path: /metadata/annotations/storageclass.kubernetes.io~1is-default-class + value: "true" \ No newline at end of file diff --git a/manuals/deploy_k8s_components.md b/manuals/deploy_k8s_components.md new file mode 100644 index 0000000..a56d916 --- /dev/null +++ b/manuals/deploy_k8s_components.md @@ -0,0 +1,732 @@ +### Getting started +kubectl apply -k . # from root project folder +if not detected: kubectl get pods -n db +kubectl apply -f db/yugabytedb.yaml -n db + +- Find services and its ports +kubectl get svc -n apps + +- Port forwarding local to cosmo-router,argo-cd +kubectl port-forward svc/cosmo-router 3002:3002 -n apps + +kubectl port-forward svc/argocd-server -n argocd 8480:443 + + +- Open yugabyte +kubectl exec -it yb-tserver-0 -n db -- ysqlsh -h +yb-tserver-0 + +- Open nats + kubectl run nats-tools --image=natsio/nats-box -n infra --rm -it -- nats stream info request_stream --server=nats://nats-cluster:4222 + + if already exist: + + kubectl exec -it nats-box -n infra -- nats stream info request_stream -s nats://nats-cluster:4222 + - list stream: + kubectl exec -it nats-box -n infra -- nats -s nats://nats-cluster.infra.svc.cluster.local:4222 stream ls + - **add stream**: + kubectl exec -it nats-box nats -n infra -- /bin/sh + + nats -s nats://nats-cluster.infra.svc.cluster.local:4222 stream add request_stream --subjects "input_request_logs" --ack --storage file --retention limits --max-msgs=-1 --max-bytes=-1 --max-age=1y --replicas 3 + + check: + kubectl describe pod nats-cluster-1 -n infra + +- Check benthos +kubectl logs -f benthos-556f97988-cgd7f -n infra +- Redeploy +For cosmo-router under namespace apps: +``` +to apply changes: + helm upgrade cosmo-router ./cosmo-router -n apps # from folder apps +we can not use this command to apply changes: + kubectl rollout restart deployment cosmo-router -n apps +``` + + + +### Install Linkerd on local machine +Linkerd exe is installed in your folder of choice, and set +environment variables path to refer to this. + +### Some linkerd commands +Some commands: +linkerd viz dashboard +linkerd version --client +linkerd check --linkerd-namespace apps +linkerd -n apps stat deployments +linkerd check --pre + + +### Deinstalling Linkerd: +- Deleting Linkerd (depends on namespace) : +``` +- kubectl delete deployment linkerd-destination linkerd-identity linkerd-proxy-injector -n apps +- kubectl delete deployment metrics-api prometheus tap tap-injector web -n apps +- Delete the Mutating Webhooks (CRITICAL): + - kubectl delete mutatingwebhookconfiguration linkerd-proxy-injector-webhook-config + - kubectl delete mutatingwebhookconfiguration linkerd-tap-injector-webhook-config + - kubectl delete validatingwebhookconfiguration linkerd-sp-validator-webhook-config +- Remove the annotation from your namespace: + kubectl annotate namespace apps linkerd.io/inject- +- Restart your apps to strip the sidecars: + kubectl rollout restart deployment cosmo-router -n apps + kubectl rollout restart statefulset nats-cluster -n infra +``` +- Deleting Linkerd using Helm installation: +``` +# 1. Delete the current broken Linkerd installations +helm uninstall linkerd-viz -n apps +helm uninstall linkerd-control-plane -n apps +helm uninstall linkerd-crds -n apps + +# 2. IMPORTANT: Delete any leftover Linkerd webhooks (these often block restarts) +kubectl delete mutatingwebhookconfiguration linkerd-proxy-injector-webhook-config +kubectl delete validatingwebhookconfiguration linkerd-sp-validator-webhook-config +``` +- Deleting Linkerd using linkerd CLI: + linkerd uninstall --namespace apps | kubectl delete -f - + +### Open source Helm chart +Most of the time we do not need to write k8s yaml files. +If we have helm chart available from the provider we generate the files needed using helm template and refer the generated file from kustomization.yaml + +#### Using helm chart vs kustomize +if you want to move your cluster to new VMs, you just run: +To run with helm chart: kubectl apply -k . + +Kubernetes will create the namespaces, then deploy NATS, then Benthos, then Yugabyte all in one go + +|Method|Pro|Con| +|--|---|--| +|helm install|Fast and easy for one-offs.|Hard to remember settings; hard to track in Git.| +|Kustomize + Helm|Single source of truth; everything in Git.|Requires --enable-helm flag; slightly more setup.| +- Manual way with helm chart + We also have 2 ways by using helm template vs helm install +``` +helm template yugabytedb yugabytedb/yugabyte ` + --namespace db ` + --set storage.master.storageClass=longhorn ` + --set storage.tserver.storageClass=longhorn ` + --set replicas.master=3 ` + --set replicas.tserver=3 ` + --set enableLoadBalancer=false ` + --set gflags.master.max_clock_skew_usec=2000000 ` + --set gflags.tserver.max_clock_skew_usec=2000000 ` + --set gflags.master.time_source=system ` + --set gflags.tserver.time_source=system ` + --set gflags.tserver.start_pgsql_proxy=true ` + > db/yugabytedb.yaml + + OR +helm install yugabytedb yugabytedb/yugabyte ` + --namespace db ` + --set storage.master.storageClass=longhorn ` + --set storage.tserver.storageClass=longhorn ` + --set replicas.master=3 ` + --set replicas.tserver=4 ` + --set gflags.master.max_clock_skew_usec=2000000 ` + --set gflags.tserver.max_clock_skew_usec=2000000 ` + --set gflags.master.time_source=system ` + --set enableLoadBalancer=false ` + --set gflags.tserver.start_pgsql_proxy=true ` + --set gflags.tserver.time_source=system + +``` +|Feature|helm template|helm install| +|---|---|----| +|Action|Local Only. Generates raw Kubernetes YAML and prints it |to a file.|Live. Sends the YAML directly to your cluster API.| +|Result|"You get a file (db/yugabytedb.yaml) that you can |inspect| edit| or commit to Git."|The database starts running |immediately in your db namespace.| +|Usage|Best for GitOps/Kustomize. You use the generated file as |a resource in Kustomize.|Best for Quick deployment. Harder to track changes over time in Git.| +- Kustomize way but required installing standalone Kustomization binary file on windows +`./kustomize build . --enable-helm | kubectl apply -f -` + +We do not use Kustomize here +#### NATS +``` +helm repo add nats https://nats-io.github.io/k8s/helm/charts/ +helm repo update + +- Basic +nats-values.yaml +config: + jetstream: + enabled: true + fileStore: + pvc: + size: 5Gi # Reserve 5GB for JetStream + +or + +- HA +# To increase replicas for High Availability +replicaCount: 3 + +# JetStream Clustering (The "Replica Factor") +config: + cluster: + enabled: true + jetstream: + enabled: true + # This ensures your data survives if x-vm2 goes down + fileStore: + pvc: + enabled: true + storageClassName: "local-path" # k3s default storage + size: 10Gi + +Check: +# Create the namespace first +kubectl create namespace infra + +# Install NATS using the chart and your values file +helm install nats-cluster nats/nats --namespace infra -f nats-values.yaml + +or with helm template +helm template nats-cluster nats/nats ` + --namespace infra ` + --set replicaCount=3 ` + --set config.cluster.enabled=true ` + --set config.jetstream.enabled=true ` + --set config.jetstream.fileStore.pvc.enabled=true ` + --set config.jetstream.fileStore.pvc.storageClassName="longhorn" ` + --set config.jetstream.fileStore.pvc.size=10Gi ` + > infra/nats-cluster.yaml + +Apply the template (without creating cluster) +kubectl apply -f infra/nats-cluster.yaml + +Apply the template( with creating cluster pods, service, etc): +helm install nats-cluster nats/nats --namespace infra -f nats-values.yaml + +After modification of other files you can apply the changes by: +helm upgrade nats-cluster nats/nats --namespace infra -f nats-values.yaml + +Remove nats-cluster +helm uninstall nats-cluster -n infra + +Remove pvc of nats-cluster (first uninstall nats-cluster using helm-uninstall) +kubectl delete pvc -n infra -l app.kubernetes.io/instance=nats-cluster + +Verify: +kubectl get all -n infra +kubectl get statefulset -n infra + +kubectl get pods -n infra -w +kubectl get pvc -n infra + +kubectl get sc # verify longhorn is ready and status healthy + +To avoid typing -n infra every time, you can switch your "active room" to infra permanently: +kubectl config set-context --current --namespace=infra + + +Apply changes: +helm upgrade nats-cluster nats/nats -n infra -f nats-values.yaml + +Testing: +first terminal: +kubectl run nats-box --image=natsio/nats-box:latest -n infra -it --rm +nats sub -s nats-cluster test.topic +new terminal: +# Instead of 'run', we 'exec' into the pod that is already there +kubectl exec -n infra -it nats-box -- /bin/sh +nats pub -s nats-cluster test.topic "Hello from Country B!" + +or + +kubectl exec -it nats-box -n infra -- nats -s nats-cluster:4222 pub input_request_logs '{"content": {"hash": "test-123"}, "message": "Hello Benthos!"}' + +Delete: +kubectl delete statefulset nats-cluster -n infra +kubectl delete all -l app.kubernetes.io/instance=nats-cluster -n default + +``` + +#### Intrepretation +``` +kubectl get all -n infra +NAME READY STATUS RESTARTS AGE +pod/nats-box 1/1 Running 0 13m +pod/nats-cluster-0 2/2 Running 0 87s +pod/nats-cluster-1 2/2 Running 0 87s +pod/nats-cluster-2 2/2 Running 0 87s +pod/nats-cluster-box-868cc6c48b-dlvtn 1/1 Running 0 26m + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/nats-cluster ClusterIP 10.43.208.167 4222/TCP 26m +service/nats-cluster-headless ClusterIP None 4222/TCP,6222/TCP,8222/TCP 26m + +NAME READY UP-TO-DATE AVAILABLE AGE +deployment.apps/nats-cluster-box 1/1 1 1 26m + +NAME DESIRED CURRENT READY AGE +replicaset.apps/nats-cluster-box-868cc6c48b 1 1 1 26m + +NAME READY AGE +statefulset.apps/nats-cluster 3/3 88s + +Interpretation: +The "Power Trio" (StatefulSet) +pod/nats-cluster-0 +pod/nats-cluster-1 +pod/nats-cluster-2 + +Why: The official Helm chart defaults to replicaCount: 3. This is for High Availability. Since you have 4 VMs, Kubernetes likely spread these across your different nodes. If one VM in Country B fails, NATS will keep running because the other two pods have a "quorum" (majority) and won't lose your data. + +The "Utility" Pods (Box) +pod/nats-box: This is the temporary pod you created manually with kubectl run to test the connection. It stays there until you delete it or it finishes. + +pod/nats-cluster-box-868cc6c48b-dlvtn: This was created automatically by the Helm chart. The NATS team includes a permanent "box" deployment so you always have a toolset inside the cluster to check the status of the NATS stream. + +What about the "2/2" READY status? +You'll notice the nats-cluster-x pods say 2/2. This means each pod actually contains two containers: + +The NATS Server: The actual engine. + +The NATS Config Reloader: A "sidecar" container that watches for changes to your settings and tells the NATS server to refresh without restarting. + + +``` +#### Rules of Engagement +Creating stream with Benthos for how NATS stores your data +``` +? Retention Policy Limits, WorkQueue, Interest +? Discard Policy Old vs New +In NATS, -1 means "Infinite" or "No Limit." +? Stream Messages Limit -1 +? Per Subject Messages Limit -1 +? Total Stream Size -1 +? Message TTL -1 +? Max Message Size -1 +? Duplicate tracking time window 2m0s +? Allow message Roll-ups Yes +? Allow message deletion Yes +? Allow purging subjects or the entire stream (Y/n) + +nats -s nats-cluster:4222 stream add request_stream --subjects "input_request_logs" --ack --storage file --retention limits --max-msgs=-1 --max-bytes=-1 --max-age=1y --replicas 3 + +nats -s nats-cluster:4222 stream add transaction_stream --subjects "input_transaction_logs" --ack --storage file --replicas 3 +``` +### Benthos +``` +# Running namespaces +kubectl apply -f namespaces.yaml # run this command from the root of your repo + +# Running Kustomization Deploy configs +kubectl apply -k . -n infra # from infra\benthos folder +kubectl apply -k .\benthos\ -n infra # from infra folder + +# Manual creation of config map +kubectl create configmap benthos-config --from-file=./benthos-configs/ + +# Verify ConfigMaps +Check if your files were successfully "packed" into Kubernetes: +kubectl get configmap -n infra + + +# To see the actual content of your streams inside K8s: +kubectl describe configmap benthos-streams -n infra +# Verify the 3 Replicas +kubectl get pods -n infra -l app=benthos +# Verify the Dashboard (Port-Forward) To see the Benthos UI on your laptop: +kubectl port-forward svc/benthos-ui -n infra 4195:4195 + +# Peek inside the running Pod: +kubectl exec -it -n infra -- ls /configs/streams +kubectl exec -it -n infra -- ls -R /configs +# Verify +kubectl get svc -n infra. +# Create Stream: +kubectl exec -n infra -it nats-box -- /bin/sh + +``` +#### Debugging +``` +kubectl describe pod benthos-68cb959d58-47prd -n infra + +kubectl logs benthos-86f5c886b7-5fr5t -n infra + +If needed delete only deployment and redeploy using kustomization +Depends on the your position +kubectl delete deployment benthos -n infra +kubectl apply -k .\infra\benthos\ -n infra +or only without namespaces +kubectl apply -k .\infra\benthos\ +or only (if you are inside \infra\benthos\) +kubectl apply -k . + +# You must not delete configmap since it get refreshed everytime you changed. Optional: + +kubectl delete configmap --all -n infra (Only if only Benthos is in infra) +# Check output after redeploy +kubectl get pod -n infra -o yaml +kubectl kustomize . +``` + +### Installing Longhorn on each VMs +- Prerequisite +You need to run the following command on all 4 VMs (Master and all Workers) to ensure Longhorn can communicate with the disks: +``` +sudo apt install open-iscsi nfs-common util-linux -y +sudo systemctl enable --now iscsid + +Why do we need this? Longhorn creates "Block Devices." To do that, the Linux kernel on your VM needs open-iscsi to "attach" to the virtual disks Longhorn creates. Without this, your pods will be stuck in ContainerCreating forever + + +1. Add the repo +helm repo add longhorn https://charts.longhorn.io +helm repo update + +2. Install into a new namespace + +Longhorn is a "System Service" not Application Infrastructure, requires high privileges +helm install longhorn longhorn/longhorn --namespace longhorn-system --create-namespace + +- Check : kubectl get pods -n longhorn-system + +3. Running the StorageClass Patch from Local Windows Laptop +Once installed, tell K3s to use Longhorn for every database deployed: + +kubectl patch storageclass longhorn -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}' + +or +kubectl patch storageclass longhorn -p "{\"metadata\": {\"annotations\":{\"storageclass.kubernetes.io/is-default-class\":\"true\"}}}" + +or + Run kubectl edit storageclass longhorn + + Look for the metadata: section. + + Add this line under annotations: + storageclass.kubernetes.io/is-default-class: "true" + + Save and exit. + +- Notes +Because you have 4 VMs, Longhorn will default to 3 replicas for your data. This is perfect. It means: + +Data is written to VM1. + +Longhorn clones it to VM2 and VM3. + +VM4 stays as a "spare" or handles other data. + +If any one VM catches fire, your data is still 100% safe and available. + + +4. UI Longhorn: +A. Update your Windows Hosts file: +- Open Notepad (Make sure you have right to write) + +- Open C:\Windows\System32\drivers\etc\hosts. + +- Add the IP of your Master VM: +192.168.x.x longhorn.local (Replace with your actual VM IP) + +B. Create longhorn.ingress.yaml and run: +/country-b-cluster-ops + /system <-- NEW FOLDER for cluster-wide tools + longhorn-ingress.yaml + /infra <-- For your messaging/processing (NATS, Benthos) + /db <-- For your databases (Yugabyte) +kubectl apply -f system/longhorn-ingress.yaml + +C. Open http://longhorn.local from your laptop +``` + +#### Additional Configuration? +There is one common issue with K3s and Longhorn. K3s stores its data in /var/lib/rancher/k3s/storage, but Longhorn defaults to /var/lib/longhorn. + +Recommendation: +If you have a specific large hard drive or partition on your VMs where you want the data to live, you can configure that in the Longhorn UI. If you just have one big / partition, the default is fine. + +### Yugabyte +``` +helm repo add yugabytedb https://charts.yugabyte.com +helm repo update +helm install yugabytedb yugabytedb/yugabyte ` + --namespace db ` + --set storage.master.storageClass=longhorn ` + --set storage.tserver.storageClass=longhorn ` + --set replicas.master=3 ` + --set replicas.tserver=4 ` + --set gflags.master.max_clock_skew_usec=2000000 ` + --set gflags.tserver.max_clock_skew_usec=2000000 ` + --set gflags.master.time_source=system ` + --set enableLoadBalancer=false ` + --set gflags.tserver.start_pgsql_proxy=true ` + --set gflags.tserver.time_source=system + + +or + +helm template yugabytedb yugabytedb/yugabyte ` + --namespace db ` + --set storage.master.storageClass=longhorn ` + --set storage.tserver.storageClass=longhorn ` + --set replicas.master=3 ` + --set replicas.tserver=4 ` + --set gflags.master.max_clock_skew_usec=2000000 ` + --set gflags.tserver.max_clock_skew_usec=2000000 ` + --set gflags.master.time_source=system ` + --set enableLoadBalancer=false ` + --set gflags.tserver.start_pgsql_proxy=true ` + --set gflags.tserver.time_source=system > yugabytedb.yaml +``` +- set gflags.tserver.start_pgsql_proxy=true? +This flag tells the Yugabyte T-Server to turn on the YSQL API layer + +- Fully Qualified Domain Name FQDN for communication + - yugabytedb.db.svc.cluster.local + + - The pattern is: `[service-name].[namespace].svc.cluster.local` +#### Optimal Node Distribution for 4 VMs +``` +For a 4-node cluster, you have enough overhead to ensure High Availability (HA) while maximizing resource use. + +Master Count: 3 + +Why: Yugabyte Masters use the Raft consensus algorithm. You need an odd number to avoid "split-brain" scenarios. 3 masters can tolerate 1 node failure. Since you have 4 VMs, you should stick with 3. + +T-Server Count: 3 or 4 + +Option 3 (Balanced): Keep 3 T-Servers. This leaves 1 VM entirely free for Benthos, NATS, and Redpanda. + +Option 4 (High Performance): Run 4 T-Servers. This spreads your data shards across all available hardware. + +Recommendation: Go with 3 Masters and 3 T-Servers. This keeps the "Replication Factor" (RF) at 3, which is the standard. Adding a 4th T-Server with RF3 is possible, but 3/3 is more predictable for a small cluster. + +``` + +### Linkerd, Viz, openssl on k8s +``` +1. Create the Trust Anchor (Root CA) +# Generate private key for the Root +a.openssl ecparam -name prime256v1 -genkey -noout -out ca.key +b.openssl ecparam -name prime256v1 -genkey -noout -out ca.key +# Generate the self-signed Root Certificate (Valid for 10 years) +a.openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \ + -out ca.crt -subj "/CN=root.linkerd.cluster.local" \ + -addext "basicConstraints=critical,CA:TRUE" +b.openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt -subj "/CN=root.linkerd.cluster.local" -addext "basicConstraints=critical,CA:TRUE" + +2. Create the Identity Issuer (Intermediate CA) +# Generate private key for the Issuer +a/b. openssl ecparam -name prime256v1 -genkey -noout -out issuer.key + +# Create a CSR (Certificate Signing Request) +a/b. openssl req -new -key issuer.key -out issuer.csr -subj "/CN=identity.linkerd.cluster.local" + +# Create a config file for the intermediate CA extensions +a. cat > issuer.ext < ext.txt +echo "keyUsage=critical,digitalSignature,keyCertSign,cRLSign" >> ext.txt +openssl x509 -req -in issuer.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out issuer.crt -days 365 -sha256 -extfile ext.txt + +3. Generate k3s Manifests +helm repo add linkerd https://helm.linkerd.io/stable && helm repo update + +4. Generate CRDs +Linkerd Custom Resource Definitions must be installed first. +helm template linkerd-crds linkerd/linkerd-crds --namespace linkerd > linkerd-crds.yaml + +5. Generate Control Plane +We will bake your certificates directly into the generated YAML: +``` +helm template linkerd-control-plane linkerd/linkerd-control-plane +--namespace linkerd +--set-file identityTrustAnchorsPEM=ca.crt +--set-file identity.issuer.tls.crtPEM=issuer.crt --set-file identity.issuer.tls.keyPEM=issuer.key +--set identity.issuer.scheme=kubernetes.io/tls > linkerd-control-plane.yaml + +or + +helm install linkerd-control-plane linkerd/linkerd-control-plane + -n linkerd + --set-file identityTrustAnchorsPEM=ca.crt + --set-file identity.issuer.tls.crtPEM=issuer.crt + --set-file identity.issuer.tls.keyPEM=issuer.key +``` +6. Generate Viz (Dashboard) +Default namespaces are linkerd and linkerd-viz but we save them under our namespace apps +``` +a. helm template linkerd-viz linkerd/linkerd-viz --namespace linkerd-viz > linkerd-viz.yaml (standard namespace) + +b. helm template linkerd-viz linkerd/linkerd-viz \ + --namespace apps \ + --set linkerdNamespace=apps \ + --set tap.namespace=apps \ + --set dashboard.namespace=apps \ + --set prometheus.namespace=apps > linkerd-viz.yaml + +or using +c. helm install linkerd-viz linkerd/linkerd-viz -n linkerd-viz --create-namespace + +Uninstalling: +helm ls -n apps + +helm uninstall linkerd-viz -n apps +helm uninstall linkerd-control-plane -n apps +helm uninstall linkerd-crds -n apps + + +``` + + +7. Creating k8s Secret in our namespace +the pod is looking for secret in the apps namespace before the pod can start: +kubectl create secret tls linkerd-identity-issuer --cert=issuer.crt --key=issuer.key --namespace=linkerd +or +kubectl create secret tls linkerd-identity-issuer --cert=issuer.crt --key=issuer.key --namespace=apps + +8.. Apply CRDs first +kubectl apply -f linkerd-crds.yaml + +9. Apply Control Plane +kubectl apply -f linkerd-control-plane.yaml +10. Apply Viz +kubectl apply -f linkerd-viz.yaml + +11. Activating the Connection (Injection) +kubectl annotate namespace apps linkerd.io/inject=enabled + +12. Check pods status + +kubectl get pods -n apps +Redeploy if error + +linkerd-destination-6b69957545-9b9tz 0/4 CrashLoopBackOff 720 (2m23s ago) 12h +linkerd-destination-74cc587f5-gvgqd 0/4 CrashLoopBackOff 712 (50s ago) 12h +linkerd-identity-68858f6c75-t2lk9 2/2 Running 0 14h +linkerd-proxy-injector-54bc495b55-sgk6l 0/2 CrashLoopBackOff 309 (2m23s ago) 12h +linkerd-proxy-injector-584f78bf9-86r64 0/2 CrashLoopBackOff 306 (60s ago) 12h + +- destination: The main service discovery logic. +-policy: Handles authorization policies. +-linkerd-proxy: The "sidecar" that handles the pod's own traffic. +-linkerd-init: (Finished) Sets up network rules. + +13.Rollout Restart: +For existing services (like Yugabyte or your Python subgraph) to join the mesh, they need to be restarted: + +kubectl rollout restart deployment cosmo-router -n apps +kubectl rollout restart statefulset yugabytedb -n db + +12. How to Verify +To verify the connection and see your Yugabyte/Redpanda traffic: + +Check Status: linkerd check (Requires Linkerd CLI: curl -sL https://run.linkerd.io/install | sh). + +Open Dashboard: linkerd viz dashboard. + +Check mTLS: In the dashboard, look for the shield icon next to your cosmo-router to subgraph traffic. This confirms your OpenSSL certificates are working. + +Debugging: + +kubectl describe pod -n apps -l linkerd.io/control-plane-component=identity +openssl x509 -in ca.crt -text -noout +kubectl logs -n apps deployment/linkerd-identity + + +``` + +#### Linkerd Activation / Deactivation +``` +# Run these commands to tell Linkerd that these namespaces are now part of the mesh: +kubectl annotate namespace apps linkerd.io/inject=enabled +kubectl annotate namespace infra linkerd.io/inject=enabled +kubectl annotate namespace db linkerd.io/inject=enabled +kubectl annotate namespace stream linkerd.io/inject=enabled + +# Restart everything in those namespaces +kubectl rollout restart deployment -n apps +kubectl rollout restart deployment -n infra +kubectl rollout restart statefulset -n infra # For your NATS cluster +kubectl rollout restart statefulset -n db # For your Yugabyte/DB +kubectl rollout restart deployment -n stream + +# Remove the annotation to deactivate +kubectl annotate namespace apps linkerd.io/inject- +kubectl annotate namespace infra linkerd.io/inject- +kubectl annotate namespace db linkerd.io/inject- +kubectl annotate namespace stream linkerd.io/inject- + +# Clean the pods +kubectl rollout restart deployment -n apps +kubectl rollout restart deployment -n infra +kubectl rollout restart statefulset -n infra +kubectl rollout restart statefulset -n db + +``` +### Cosmo router +``` +1. Check +cosmo-router/ +├── config.yaml <-- Your router config +├── router.json <-- Your composed graph +├── values.yaml <-- Updated with Ingress and Middleware annotations +└── templates/ + ├── _helpers.tpl <-- Required for names + ├── configmap.yaml <-- Uses .Files.Get(our custom config) + ├── deployment.yaml <-- Incl Checksum & Linkerd injection (The brain) + ├── service.yaml (The external network) + ├── ingress.yaml (The internal network) + └── middleware.yaml <-- The Gzip logic + └── hpa.yaml optional + +Delete: +a. httproute.yaml (Causes the current error) +b.serviceaccount.yaml (Caused the previous error) +c. tests/ (The default test folder often causes similar issues) + +2. Test it +helm template cosmo-router ./cosmo-router + +3. Run cosmo-router +# Deploy it +helm install cosmo-router ./cosmo-router -n apps + +or Redeploy +helm upgrade cosmo-router ./cosmo-router -n apps + +or If needed uninstall +helm uninstall linkerd-crds -n apps +helm uninstall cosmo-router -n apps + +Some usefult commands: +kubectl delete pods -n apps -l linkerd.io/extension=viz +kubectl annotate namespace apps linkerd.io/inject=enables +kubectl rollout restart deployment cosmo-router -n apps +kubectl logs -n apps -l app.kubernetes.io/name=cosmo-router +``` +#### Helm chart: Which files to Keep, Adapt, or Delete? +Since we ran helm create, you have a lot of "boilerplate." +``` +5. Keep and Adapt: +deployment.yaml: Ensure the volumeMounts and volumes match the ConfigMap name above. + +service.yaml: Keep it to allow other pods (or the Ingress) to find the router. + +values.yaml: Use this for your image tags and environment-specific toggles. + +_helpers.tpl: Do not delete. This generates the names (like cosmo-router.fullname) used in every other file. + +Keep (but leave disabled): +hpa.yaml: Useful later for auto-scaling, but set autoscaling.enabled: false in values.yaml for now. + +ingress.yaml: Keep this if you want to access the router from outside the cluster (e.g., router.example.com). + +Delete: +httproute.yaml: This is for the "Gateway API." Unless you have a specific Gateway controller installed, standard ingress.yaml is what you'll use. + +NOTES.txt: Usually just contains generic text. +```` \ No newline at end of file diff --git a/manuals/important_commands.md b/manuals/important_commands.md new file mode 100644 index 0000000..eed0043 --- /dev/null +++ b/manuals/important_commands.md @@ -0,0 +1,139 @@ +``` +kubectl run nats-box--image=natsio/nats-box -n infra --rm +--overrides='{\"metadata\": {\"annotations\": {\"linkerd.io/inject\": \"enabled\"}}}' +--stdin --tty +--nats -s nats://nats-cluster:4222 stream add request_stream --subjects \"input_request_logs\" --ack --storage file --retention limits +--max-msgs=-1 +--max-bytes=-1 +--max-age=1y +--replicas 3 + +kubectl run nats-box --image=natsio/nats-box -n infra --rm -it --overrides='{\"metadata\": {\"annotations\": {\"linkerd.io/inject\": \"enabled\"}}}' -- nats stream info request_stream -s nats://nats-cluster:4222 + +(kubectl run nats-box --image=natsio/nats-box -n infra --dry-run=client -o yaml -- nats -s nats://nats-cluster:4222 stream add request_stream --subjects "input_request_logs" --ack --storage file --retention limits --max-msgs=-1 --max-bytes=-1 --max-age=1y --replicas 3 | linkerd inject - | kubectl apply -f -) + + +Listen to topics: +kubectl exec -it nats-box -n infra -- nats -s nats://nats-cluster:4222 sub "input_request_logs" --stream request_stream + +To view existing messages: +(nats) stream view request_stream --subject input_request_logs. + +To check consumer status: +(nats) consumer info request_stream . + + +Info of clusters +kubectl exec -it nats-cluster-0 -n infra -c nats -- /bin/sh # Exec into the FIRST pod (0) +kubectl exec -it nats-box -n infra -- nats -s nats://nats-cluster:4222 stream ls +kubectl exec -it nats-box -n infra -- nats -s nats://nats-cluster.infra.svc.cluster.local:4222 stream ls +kubectl exec -it nats-box -n infra -- nats -s nats://nats-cluster:4222 stream info request_stream +kubectl exec -it nats-cluster-0 -n infra -c nats -- netstat -tuln +kubectl get pod nats-cluster-0 -n infra -o wide + +kubectl get endpoints nats-cluster -n infra + + + +kubectl exec -it nats-box -n infra -- nats stream add request_stream --subjects "input_request_logs" --ack --storage file --retention limits --max-msgs=-1 --max-bytes=-1 --max-age=1y --replicas 3 --server nats://nats-cluster:4222 + +Apply modification: +helm upgrade nats-cluster nats/nats --namespace infra -f nats-values.yaml + +Some commands: +kubectl get pods -n infra --show-labels +kubectl delete pod nats-cluster-0 nats-cluster-1 nats-cluster-2 -n infra +kubectl get volumes.longhorn.io -n longhorn-system +kubectl get sc # for longhorn +kubectl describe pod -n apps cosmo-router-675c8666cb-cmt82 + +kubectl logs -n apps -l linkerd.io/control-plane-component=identity +kubectl logs -n apps -l app.kubernetes.io/name=cosmo-router +kubectl delete pods -n apps -l linkerd.io/extension=viz +kubectl delete all -l app.kubernetes.io/instance=nats-cluster -n default +kubectl delete pod nats-box -n infra --force +kubectl delete pvc -n infra -l app.kubernetes.io/instance=nats-cluster + +Activate linkerd: +kubectl annotate namespace apps linkerd.io/inject=enabled +kubectl annotate namespace infra linkerd.io/inject=enabled +kubectl annotate namespace db linkerd.io/inject=enabled +kubectl annotate namespace stream linkerd.io/inject=enabled + +Inject into pods: +kubectl rollout restart deployment cosmo-router -n apps # only injects to cosmo-router +# Restart everything in those namespaces +kubectl rollout restart deployment -n apps +kubectl rollout restart deployment -n infra +kubectl rollout restart statefulset -n infra # For your NATS cluster +kubectl rollout restart statefulset -n db # For your Yugabyte/DB +kubectl rollout restart deployment -n stream + +Example: After updating benthos streams or templates +kubectl annotate pod -l app=benthos linkerd.io/inject=disabled --overwrite -n infra +kubectl rollout restart deployment benthos -n infra + +Deactivate linkerd: +kubectl annotate namespace apps linkerd.io/inject- +kubectl annotate namespace infra linkerd.io/inject- +kubectl annotate namespace db linkerd.io/inject- +kubectl annotate namespace stream linkerd.io/inject- + +Clean the pods: +kubectl rollout restart deployment -n apps +kubectl rollout restart deployment -n infra +kubectl rollout restart statefulset -n infra +kubectl rollout restart statefulset -n db + +Check which namespaces/pods are set for the mesh: +kubectl get ns -o custom-columns=NAME:.metadata.name,INJECT:.metadata.annotations."linkerd\.io/inject" +kubectl get pods -A -o custom-columns=NAME:.metadata.name,NAMESPACE:.metadata.namespace,MESHED:.metadata.annotations."linkerd\.io/inject" +kubectl get namespaces -L linkerd.io/inject +kubectl get pod -n apps -o yaml | grep "linkerd.io/inject" +kubectl get pods -A -l linkerd.io/control-plane-ns + +Linkerd commands: +linkerd check # Ensure the Linkerd system itself is healthy. +linkerd check --linkerd-namespace apps +linkerd -n apps check --proxy + +linkerd stat ns +linkerd viz dashboard # View the dashboard in your browser +linkerd viz stat namespace apps Check stats for your specific namespace +linkerd viz edge -n apps # See real-time traffic between Cosmo-Router and NATS + +linkerd viz tap -n infra deploy/benthos +linkerd viz stat -n apps deploy/cosmo-router +linkerd viz tap deployment/benthos -n infra --to deployment/nats-cluster # Run this to see if the Linkerd proxies are actually passing traffic: + + + +Important: The "Skip" Configuration + +metadata: + annotations: + linkerd.io/inject: disabled + +kubectl exec -it deployment/benthos -n infra -- env | findstr ECOM_PASS +kubectl logs deployment/benthos -n infra | findstr "sql_poller" + +first run kubectl exec -it nats-box-temp nats -n infra -- /bin/sh +Then the followings: +nats str view fact_stream -s nats://nats-cluster:4222 +nats consumer next fact_stream my_consumer_name -s nats://nats-cluster:4222 +nats sub "output_request_logs" -s nats://nats-cluster:4222 +nats stream ls -s nats://nats-cluster:4222 +nats stream info fact_stream -s nats://nats-cluster:4222 + +# Check if other pods on that node can reach the internet +kubectl run net-test --image=alpine --restart=Never -n infra -- nslookup google.com + + +kubectl exec -it yb-tserver-0 -n db -- ysqlsh -h +kubectl port-forward svc/cosmo-router 3002:3002 -n apps +kubectl port-forward svc/postgres-ha-rw 5436:5432 -n db +``` +#### The Difference: NATS Core vs. JetStream +- NATS Core (nats sub): This is "Fire and Forget." If no one is listening when Benthos sends the message, the message disappears forever. + +- NATS JetStream: This is a Stream. It saves the messages to disk. To get data from a stream, you don't use sub; you use a Consumer. \ No newline at end of file diff --git a/manuals/interconnection.md b/manuals/interconnection.md new file mode 100644 index 0000000..36a6012 --- /dev/null +++ b/manuals/interconnection.md @@ -0,0 +1,94 @@ +#### Overview + +|Feature|kubectl port-forward|SSH Tunnel (-L)|VPN (Wireguard/Tailscale)| +|---|---|---|----| +|Setup|Zero (if you have kubectl)|Requires SSH access|Requires Client/Driver install| +|Scope|Single Port/Service|Single Port/Service|Entire Network| +|Lifetime|Temporary (dies if cmd closes)|Temporary|Persistent/Background| +|OS Level|User-space (Application)|User-space (Application)|Kernel-space (Network Card)| + +From a technical point of view, kubectl port-forward is most accurately described as a User-Space Application Proxy or a Layer 7 Tunnel. + +It is not a full-blown VPN or a network-level tunnel like Wireguard. Instead, it is a "targeted bridge" created specifically for one-way traffic between your local machine and a specific service inside the cluster. + + +1. Closest Match: SSH Local Port Forwarding (ssh -L) +This is the "twin brother" of kubectl port-forward. + +How it's similar: You use an existing encrypted management channel (SSH or the Kubernetes API) to "piggyback" traffic for a specific port. Neither creates a new network interface on your machine; they just listen on a local port and "teleport" data to the other side. + +The Comparison: Just as ssh -L 5436:localhost:5432 user@server maps a remote DB to your laptop, kubectl does the same for a Pod. + +2. Cloudflare Tunnel (cloudflared) +How it's similar: Both allow you to bypass firewalls and NAT without opening public ports. They both rely on an "outbound" connection from the cluster/server to a central controller to establish the path. + +The Difference: Cloudflare Tunnels are designed for permanent, public-facing access; port-forward is a temporary, private developer tool. + +3. What it is NOT: VPNs (OpenVPN, Wireguard, Tailscale) +Why they are different: A VPN creates a Virtual Network Interface (like tun0 or utun). When you use a VPN, your laptop actually "lives" inside the 10.x.x.x network. You could ping any Pod or Service directly by its internal IP. + +With port-forward: Your laptop doesn't know the db namespace exists. It only knows that localhost:5436 is magically connected to something else. + + + +#### Where do they live on the OSI Model? +``` +1. Layer 7 (Application Layer): kubectl port-forward and SSH Tunneling +Reason: These tools do not understand "IP addresses" or "Routing" in the traditional sense. + +When you run kubectl port-forward, the kubectl binary on your machine opens a local socket. + +It takes the data from that socket, wraps it in an HTTP/SPDY or HTTP/2 request, and sends it to the Kubernetes API server. + +The API server then unwraps that data and sends it to the Pod. + +Because the "tunneling" happens inside an application protocol (HTTP/SSH), it is a Layer 7 operation. + +Layer 3 (Network Layer): VPNs (Wireguard, OpenVPN, IPSec) +Reason: These create a Virtual Network Interface (like utun0). + +They handle IP Packets. + +If you send a ping to 10.42.0.5, the VPN intercepts that packet at the Network Layer, encrypts it, and sends it to the other side. + +It doesn't care if the data inside is Postgres, HTTP, or DNS; it only cares about the IP Header. + +Layer 2 (Data Link Layer): VXLAN, Tinc, or Bridge-mode VPNs +Reason: Some advanced VPNs can operate at Layer 2. + +They transport Ethernet Frames (including MAC addresses). + +This makes your local computer think it is plugged into the same "virtual switch" as the remote servers. + +2. Other Technologies to Connect +If port-forward is too temporary, here are the professional alternatives: + +A. Ingress Controller (Layer 7) +How it works: You define a "Route" (e.g., db.example.com). The Ingress (like Nginx or Traefik) receives the traffic and forwards it to your service. + +Layer: 7 (Application). It looks at the Hostname and Path in the HTTP/TLS header to decide where to send the data. + +B. LoadBalancer Service (Layer 4) +How it works: In a cloud environment (AWS/GCP), this gives your Service a real, reachable IP address. + +Layer: 4 (Transport). It only cares about the Port and Protocol (TCP/UDP). It doesn't look at the data inside the packets. + +Comparison: This is like a "Permanent Port-Forward" that anyone on the internet (or your VPC) can reach. + +C. Mesh VPNs / SD-WAN (Layer 3 - Overlay) +Examples: Tailscale, ZeroTier, Nebula. + +How it works: You install a small agent on your VM and your laptop. They create a "Peer-to-Peer" encrypted mesh. + +Layer: 3 (Network). It provides a stable IP address for your database that stays the same even if you move from office to home. + +Reason to use: This is the most "production-ready" version of what you are doing. It is more secure than a public LoadBalancer but more stable than a port-forward. + +``` + +|Technology|OSI Layer|Comparison| +|----|---|---| +|kubectl port-forward|Layer 7|"A temporary ""straw"" to sip data from one |specific cup.| +|Ingress|Layer 7|A ""Receptionist"" directing visitors based on the name on the |envelope.| +|LoadBalancer|Layer 4|"A ""Direct Pipeline"" to a specific door (port).| +|Tailscale / VPN|Layer 3|"A ""Secret Tunnel"" that puts your whole house next to the office.| \ No newline at end of file diff --git a/manuals/k8s_yaml.md b/manuals/k8s_yaml.md new file mode 100644 index 0000000..5120320 --- /dev/null +++ b/manuals/k8s_yaml.md @@ -0,0 +1,171 @@ +### Finding Flags +docker run --rm ghcr.io/wundergraph/cosmo/router:latest --help + +docker run --rm docker.redpanda.com/redpandadata/redpanda:v23.2.15 redpanda start --help + +docker run --rm ghcr.io/timeplus-io/proton:latest proton server --help + +Pinot uses subcommands: docker run --rm apachepinot/pinot:latest StartController -help + +docker run --rm quay.io/minio/minio:latest server --help + +#### Comparison +|Feature|Docker Compose (docker-compose.yaml)|K3s Manifest |(deployment.yaml)| +|---|----|---|---| +|Image|image: nginx:latest|image: nginx:latest| +|Env Var|environment: - DB_HOST=localhost|"env: - name: |DB_HOST value: ""localhost"""| +|Ports|"ports: - ""80:80"""|ports: - containerPort: 80 |(plus a Service object)| +|Volumes|volumes: - ./data:/app/data|volumeMounts: - mountPath: /app/data name: my-vol| + +- Kompose will generate the .yaml files (Deployments, Services, etc.) that K3s understands. It's not always perfect, but it handles about 90% of the heavy lifting. +`kompose convert -f docker-compose.yaml` + +|Category|Command Line Flag|Docker Compose Key|K8s Equivalence| +|---|---|---|--| +|Environment|"-e, --env|environment:|env: or envFrom:| +|Volumes|"-v, --volume"|volumes:|volumeMounts: & volumes:| +|Commands|(End of string)|command:|args: (usually)| +|Entrypoint|--entrypoint|entrypoint:|command| +|Networking|-p, --publish"|ports:|Service or containerPort| +#### General Pattern for Finding Flags (The "Deep Dive") +If --help doesn't work or the container exits too fast, use these three "Detective" steps: + +Inspect the Entrypoint: +docker inspect --format='{{.Config.Entrypoint}} {{.Config.Cmd}}' +This tells you exactly what script or binary is running so you know what to call with --help. + +Environment Variable Overrides: +Many modern images (Bitnami, Confluent) use env vars instead of flags. Check the env section of their DockerHub page. + +The "Dry Run" strategy: +Run the container with an interactive shell: +docker run -it --entrypoint /bin/sh +Once inside, manually run the binary with -h + +- Most modern cloud-native images follow the POSIX/GNU convention. You can generally find flags using: --help or -h + +- help (as a subcommand, common in Go/Rust tools) + +- Looking at the ENTRYPOINT in the Dockerfile. +``` +Common Patterns by Tech Stack: +Java (Flink, Pinot): Usually uses a custom shell script entrypoint. Flags are often passed as -Dproperty=value or via a conf.yaml. + +Go (Benthos, Cosmo, NATS): Very consistent. container-name --help almost always works. + +C++/Rust (Yugabyte, RisingWave): Usually binary-driven. binary-name --help works, but you must know the path to the binary (e.g., /home/yugabyte/bin/yb-tserver). +``` + +#### Finding & Placing Flags in Helm/K8s +In Kubernetes, you don't "find" K8s-specific flags inside the image. The image only cares about its own flags. You just have to decide how to pass them from the YAML. + +Where do they go? +In a Helm chart template (usually templates/deployment.yaml), flags go into the args or command section of the container spec. + +Example: Converting your Yugabyte T-Server flag +Your Compose: --tserver_master_addrs=yb-master:7100 + +In Helm values.yaml: +`tserver: + masterAddresses: "yb-master-service.db.svc.cluster.local:7100"` + +In Helm templates/statefulset.yaml: +containers: + - name: yb-tserver + image: yugabytedb/yugabyte:latest + command: ["/home/yugabyte/bin/yb-tserver"] + args: + - "--tserver_master_addrs={{ .Values.tserver.masterAddresses }}" + - "--rpc_bind_addresses=$(POD_IP):9100" # Use K8s env vars + +#### App Centric Structure +Where to run helm create? +You should run this inside your project root, usually in a directory named /charts or /deploy. + +``` +my-project/ +├── docker-compose.yml +├── python/ +├── benthos-configs/ +└── deploy/ <-- Run "helm create" here + ├── cosmo-router/ <-- Resulting folder + ├── yugabyte/ + └── airflow/ + +This structure treats Kubernetes manifests as "just another part of the code." + +Logic: Everything needed to run the cosmo-router application (code, Dockerfile, and Helm chart) stays in one place. + +Best for: Small teams where the same person writes the code and manages the deployment. +``` + + +#### Cluster Centric +|Namespace|Services (Folder)|Why?| +|--|--|--| +|db|"yugabytedb, redis-nats, elasticsearch-nats, minio, garage, postgres-airflow, proton|Persistence layers and heavy stateful workloads.| +|infra|"nats, redpanda, redpanda_console, prometheus, grafana-0, zookeeper, pinot-*|Message brokers, streaming |backplanes, and observability tools.| +|apps|"cosmo-router, subgraph-python, postgraphile, hasura, grafbase, benthos, superset, streampark, flink-*,risingwave-standalone, owl-shop, debezium-pinot|Business logic, Gateways, and ETL/Stream processing jobs.| + +``` +/k8s-infra +├── namespaces/ +│ ├── infra-ns.yaml +│ └── db-ns.yaml +├── infra/ (Benthos, NATS, etc.) +│ └── kustomization.yaml +├── db/ (YugabyteDB) +│ └── kustomization.yaml +└── apps/ + + +This structure treats the Kubernetes Cluster as a single entity, and your repository describes the state of that cluster. + +Logic: You organize by operational domains (Database, Networking, Application layers). + +Best for: Production environments, GitOps (using tools like ArgoCD), and scenarios where you have many moving parts (like your 15+ services). +``` + +|Feature|App-Centric (/deploy)|Cluster-Centric (/k8s-infra)| +|---|---|---| +|Separation of Concerns|Low. Infra and App code are mixed.|High. Clear boundaries between DBs, Infra, and Apps.| +|Blast Radius|High. A change in the app repo might trigger an infra redeploy.|Low. You can update the apps/ without touching the db/ logic.| +|Dependency Management|Difficult. Hard to see if apps is ready for the db.|Better. kustomization.yaml can order the execution |of resources.| +|RBAC (Permissions)|Hard to restrict. Everyone has access to everything.|Easy. You can give a dev access to apps-ns but lock the db-ns.| +|Scaling Complexity|Good for 1–5 services.|"Essential for your 15+ services (Redpanda, Yugabyte, etc.).| + +#### Generalizing K8s to Docker Compose +Think of a Pod in K8s as the closest relative to a Service in Docker Compose. However, K8s splits responsibilities across multiple objects: +``` +Deployment/StatefulSet: This is your docker-compose.yaml logic—how many replicas you want and what image to use. + +Service: This is your ports: section. While Compose handles networking internally, K8s requires an explicit Service object to route traffic to your pods. + +ConfigMap/Secret: This is a more robust version of the env_file: or environment: keys in Compose. + +PersistentVolumeClaim (PVC): This replaces the host-path mapping (-v /host:/container) with a request for storage that stays alive even if the pod dies. +``` + +#### Fully Qualified Domain Name/ FQDN +1. kubectl get svc -n db # check your +NAME TYPE CLUSTER-IP PORT(S) +yb-tservers ClusterIP 10.43.0.50 5433/TCP,9042/TCP +yb-masters ClusterIP 10.43.0.60 7100/TCP,7000/TCP + +2. Add your name with `.svc.cluster.local:port`: +Our namespace is db, so FQDN: yb-tservers.db.svc.cluster.local:5433 + + +#### Apply changes +``` +if you are using Kustomize: + +kubectl apply -k . -n infra +kubectl rollout restart deployment benthos -n infra +kubectl rollout status deployment benthos -n infra # check status + +if you originally installed via a Helm chart: +helm upgrade benthos -n infra -f values.yaml +``` + + diff --git a/manuals/setup_k8s.md b/manuals/setup_k8s.md new file mode 100644 index 0000000..4a1490f --- /dev/null +++ b/manuals/setup_k8s.md @@ -0,0 +1,668 @@ +![alt text](image-1.png) + +### Getting Started +``` +Phase 0: Preparation (All 4 VMs) +Before installing anything, ensure the VMs can talk to each other and have the necessary "plumbing." + +Assign Static IPs: Ensure your VMs have fixed internal IPs (e.g., 10.0.0.1 through 10.0.0.4). + +Hostname Setup: Give them clear names so you don't get confused: + +vm-master (Control Plane) + +vm-worker-1, vm-worker-2, vm-worker-3 + +Disable Swap: Kubernetes (and k3s) performs better with swap off. +sudo swapoff -a +# To make it permanent, comment out the 'swap' line in /etc/fstab +sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab + + +Phase 1: Deploying Kubernetes (K3s) +Using K3s is highly recommended here because it is lightweight and handles "flaky" or high-latency networks better than standard K8s. + +Step 1: Install Control Plane (VM1) +On your first VM in Country B, run: +curl -sfL https://get.k3s.io | sh - +# Get the Token to join other nodes +sudo cat /var/lib/rancher/k3s/server/node-token + + +Step 2a: Join Worker Nodes (VM2, VM3, VM4) +On the other three VMs, run the join command using the IP of VM1 and the token you just found: +curl -sfL https://get.k3s.io | K3S_URL=https://:6443 K3S_TOKEN= sh - + +Ours : +curl -sfL https://get.k3s.io | K3S_URL=https://192.168.3.91:6443 K3S_TOKEN=K10ad78a8da2499c55aeeb901a063ecf6ddd9261d57886e8813b96c6524f8a7b8f1::server:7a4c1e17141af2563287de23f083b07d sh - + +Verify the cluster: +sudo kubectl get nodes + +Step 2b: Label them, change x-vm2 to your server-name +kubectl label node x-vm2 node-role.kubernetes.io/worker=worker +kubectl label node x-vm3 node-role.kubernetes.io/worker=worker +kubectl label node x-vm4 node-role.kubernetes.io/worker=worker + +Step 2c :Create namespaces via your control-plane: +kubectl create namespace infra # For Minio, Redis, NATS +kubectl create namespace db # For YugabyteDB +kubectl create namespace stream # For Redpanda, RisingWave, Pinot +kubectl create namespace apps # For Airflow, Superset, Postgraphile + +Step 2d: install helm on your laptop (windows local host) +choco install kubernetes-helm # Installs Helm +choco install kubernetes-cli -y # Installs Kubectl (The CLI to talk to k3s) + +Verify +helm version +kubectl version --client + +Step 2e: setup your laptop to control the cluster: +sudo cat /etc/rancher/k3s/k3s.yaml # run in control-plane +This gives: + +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: + .... + server: https://127.0.0.1:6443 + name: default +contexts: +- context: + cluster: default + user: default + name: default +current-context: default +kind: Config +users: +- name: default + user: + client-certificate-data: + .... + + + +- The Critical Edit: + Find the line server: https://127.0.0.1:6443 + Change 127.0.0.1 to the IP address you use to SSH into that VM (ideally your Tailscale IP). Our Control-Plane VM has IP 192.165.3.10 on private network +- Copy k3s.yaml and ensure you save it in C:\Users\\.kube\config +- Log back into control plane VM (x-vm1) and restart k3s with the public IP in the "Tls-San" list: + curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--tls-san 192.165.3.91" sh + + or + + you can add insecure-skip-tls-verify: true like + apiVersion: v1 + clusters: + - cluster: + # certificate-authority-data: LS0tLS1... (REMOVE OR COMMENT THIS OUT) + insecure-skip-tls-verify: true + server: https://192.168.3.10:6443 + name: default + contexts: + .... + +Check: +kubectl cluster-info +kubectl config view +kubectl get nodes + +3. Phase 2: Connecting Gitea (The GitOps Flow) +Don't manually "push" code to the cluster. Instead, use ArgoCD. It sits inside your cluster in Country B and "pulls" changes from Gitea in Country A. + +Install ArgoCD in your new cluster: + +kubectl create namespace argocd +kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml + + +or using helm chart: + +# Add the repo first if you haven't +helm repo add argo https://argoproj.github.io/argo-helm +helm repo update + +# Generate the manifest +helm template argocd argo/argo-cd ` + --namespace argocd ` + --set server.service.type=ClusterIP ` + --set "server.extraArgs={--insecure}" ` + > argocd/argocd-manifest.yaml + +on local machine: +kubectl port-forward svc/argocd-server -n argocd 8388:443 + +user : admin +pw: x (0mZZafSJi7kyBYsX) + +To get x: +From PS: + kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | ForEach-Object { [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($_)) } + +From WSL: + kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 --decode + +Delete password: + kubectl delete secret argocd-initial-admin-secret -n argocd + +Option: Use a NodePort (Truly Permanent) +If you don't want to run port-forward every time, you can change the Service Type to NodePort. This will open a port on your VM's physical IP address. + +Update your helm template command: +--set server.service.type=NodePort ` +--set server.service.nodePortHttp=30443 + +4. Phase 3: Handling the Container Registry +Code is just text; Kubernetes needs Images (Docker images). + +Option 1 (Centralized): Enable the Gitea Container Registry on Server A1. Your CI (like Gitea Actions) builds the image in Country A and pushes it to Gitea. Your nodes in Country B then download that image. + +Option 2 (Faster): If the images are large, the cross-country download will be slow. Consider setting up a Registry Mirror or a local registry in Country B to cache the images. + +Summary Checklist +Network: Install Tailscale on all 4 VMs and Server A1 so they can see each other. + +Cluster: Use K3s to turn the 4 VMs into a single cluster. + +Deploy: Install ArgoCD on the cluster to pull code from Gitea. + +CI/CD: Use Gitea Actions to build your Docker images and store them in the Gitea Registry. +``` + +### Our Architecture: Country A to Country B +- Challenges: Deploy K8s in my server B1 with 4 virtual machines in a country B, my code lives in gitea hosted in a server A1 in other country A +- Answer :To deploy a Kubernetes cluster across 4 VMs in one country while pulling code from Gitea in another, you need a setup that accounts for latency and cross-border networking. + +The most stable and common way to do this is using K3s for the cluster and a GitOps approach (like ArgoCD) for the deployment + +``` +Key Components: +Source (Country A): Server A1 hosts Gitea. This is your "Source of Truth." + +Destination (Country B): Server B1 runs 4 VMs. You will designate 1 as the Control Plane (Master) and 3 as Worker Nodes. + +The Bridge: Since the servers are in different countries, use Tailscale or WireGuard to create a secure "Mesh" network. This allows Country B's VMs to talk to Country A's Gitea as if they were in the same room. +``` + +### When you run sudo swapoff -a, you are deactivating the use of the hard drive as "fake RAM." + +In Docker Compose: If your VM ran out of RAM, the Linux OS would start "swapping" data to the slow disk to keep the container alive. It gets very slow, but it stays up. + +In Kubernetes: K8s wants to be the "Boss" of memory. If a node is full, K8s wants to know immediately so it can move a pod to another VM (like from x-vm2 to x-vm3). If Swap is on, the node "lies" to Kubernetes saying "I still have space (on my slow disk!)," which prevents K8s from managing the cluster correctly. + +Rule of Thumb: Always keep Swap OFF for K3s/K8s to ensure your databases (Yugabyte/Redpanda) don't suddenly become 100x slower. + +### What is Argo CD and GitOps? +Think of Argo CD as a "Sync Engine" that lives inside your K3s cluster in Country B. +``` +The Role of GitOps +GitOps is a practice where your Git Repository is the "Single Source of Truth" for your infrastructure. + +In traditional CI/CD (like Jenkins), you "push" code to the cluster. + +In GitOps, the cluster "pulls" its own configuration from Git. + +Why use Argo CD? +Drift Detection: If someone manually deletes a pod in Country B, Argo CD sees it doesn't match the Git repo and automatically recreates it. + +Cross-Border Reliability: Since Country B "pulls" from Country A, the cluster doesn't need to be "reachable" from the internet. It only needs to be able to "see" Gitea. + +Audit Trail: Every change to your cluster is a Git commit. You know exactly who changed what and when + +Why ArgoCD? +If the connection between Country A and B drops for a moment, ArgoCD will simply wait and retry. It ensures your cluster eventually matches your code without you doing anything. + +Install ArgoCD in your new cluster: + +kubectl create namespace argocd +kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml + +Connect Gitea: In the ArgoCD UI, add your Gitea repository URL (using the private Tailscale IP for security). + +Define an "Application": Tell ArgoCD to watch a specific folder in your Gitea repo and deploy any .yaml files it finds there to your 4-node cluster. + +``` + +#### Utilizing ArgoCD + +Since you now have ArgoCD and your Kustomize structure ready, here is the file that makes ArgoCD "take over." + +Create a file named argocd/root-app.yaml: +``` +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: root-cluster-app + namespace: argocd +spec: + project: default + source: + repoURL: https://github.com/YOUR_USERNAME/YOUR_REPO.git + targetRevision: HEAD + path: . # Points to your root kustomization.yaml + destination: + server: https://kubernetes.default.svc + namespace: argocd + syncPolicy: + automated: + prune: true + selfHeal: true +``` +1. You run kubectl apply -k . one last time manually. + +2. You apply this root-app.yaml. + +3. ArgoCD looks at your Git repo. + +4. From now on, whenever you git push a change to Benthos, NATS, or Yugabyte, ArgoCD will see it and update the cluster automatically. You never have to run kubectl apply again. + +### Docker-compose vs k8s +|Docker Compose Concept|Kubernetes Equivalent| +|----|----| +|container_name: nats|Service name: nats| +|volumes: yb_data|PersistentVolumeClaim (PVC)| +|depends_on|initContainers (K8s doesn't have a native depends_on wait)| +|environment|ConfigMap or Secret| +|"ports: ""8090:8080"""|Service (Type: ClusterIP or LoadBalancer)| +#### Why no "Network" or "Port Mapping" 4195? +No Network: In Docker Compose, containers are isolated unless they share a network. In Kubernetes, all Pods in the same cluster can talk to each other by default. Your Benthos pod can reach NATS just by using the name nats-cluster:4222. + +Port 4195: In Kubernetes, ports inside a Deployment are just "documentation" for the cluster. To actually reach that port from your Windows laptop, you need a Service. + +Since you have 4 VMs, an Ingress will allow you to visit benthos.local or nats.local in your browser without having to use port-forward every time. +#### Converting Docker Compose to K8s: The Strategy +Do not try to convert all 30+ services at once. You should group them into Helm Charts or Kustomize folders in your Git repo. +``` +Step A: Handle the "Files" (ConfigMaps & Secrets) +In Docker Compose, you used volumes: ./prometheus/prometheus.yml. In K8s, files are stored as ConfigMaps. + +Create a ConfigMap for risingwave.toml. + +Create a ConfigMap for prometheus.yml. + +Create a ConfigMap for your Benthos templates. kubectl create configmap benthos-config --from-file=./benthos-configs/ + +Step B: The "Big Three" (StatefulSets) +Services like YugabyteDB, Redpanda, and Pinot are "Stateful." In Docker Compose, they just used local folders. In K8s, you must use a StatefulSet instead of a Deployment. + +Step C: Networking +In Compose, you had nats-network. In K8s, all Pods can talk to each other by default across namespaces using the DNS format: `..svc.cluster.local.` + +In Docker Compose, you just use the service name (e.g., nats). In Kubernetes, cross-namespace communication uses a Fully Qualified Domain Name (FQDN). + +How it works: +If Airflow (in apps) needs to talk to NATS (in infra), instead of just calling nats, it calls:nats.infra.svc.cluster.local + +Why you SHOULD use namespaces: +Organization: With a stack as massive as yours, kubectl get pods in a single namespace would result in a "wall of text" that is impossible to read. + +Resource Quotas: You can eventually limit how much RAM the stream namespace takes so it doesn't crash your db namespace. + +Security: You can set "Network Policies" later to ensure only apps can talk to db. + +Verdict: Keep the namespaces. It is a "best practice" for a stack this complex. + +``` + +``` +1. The Conversion Tool: Kompose +The fastest way to get started is using Kompose (Kubernetes + Compose). It translates docker-compose.yaml into K8s .yaml manifests (Deployments, Services, PersistentVolumeClaims). + +Steps to convert: +curl -L https://github.com/kubernetes/kompose/releases/download/v1.31.2/kompose-linux-amd64 -o kompose # Install Kompose +kompose convert -f docker-compose.yaml + +2. "Helm" (The Professional Way) +Most of the tools in your list have official "Charts" (install scripts). Instead of writing Yugabyte YAML from scratch, you use Helm: +# Example: Installing Yugabyte via Helm into your 'db' namespace +helm repo add yugabytedb https://charts.yugabyte.com +helm repo update +helm install my-yugabyte yugabytedb/yugabyte --namespace db + +3. The Better Strategy: The "Hybrid" Approach +Instead of one giant YAML file, break your deployment into Helm Charts. Most of the tools you are using have official, high-quality Helm charts that handle the complex K8s configuration for you. + +Recommendation: Use Official Charts for the "Heavy Lifters" +Don't try to manually write K8s YAML for these; use Helm: + +YugabyteDB: Use the Yugabyte Helm Chart. + +Airflow: Use the Official Apache Airflow Chart. + +Redpanda: Use the Redpanda Operator/Chart. + +RisingWave: Use the RisingWave Operator. + +Use Custom YAML for your "Logic" layer: +Use your converted Kompose files only for your custom subgraphs and configurations: + +subgraph-python + +benthos (with ConfigMaps for your configs) + +cosmo-router + +``` +### Why "Blind Conversion" will fail for your stack +Your compose file has several "Cloud Native" complexities that require manual intervention: +``` +Stateful Sets vs. Deployments: In Docker, you just list yugabytedb-1, 2, 3. In K8s, you should use a StatefulSet. This ensures that if yugabytedb-1 restarts, it attaches to the exact same disk it had before. + +Healthchecks: Your healthcheck blocks in Compose need to be converted to K8s livenessProbe and readinessProbe. + +Networking: In Compose, all services see each other via the container name. In K8s, you must ensure each Deployment has a Service object so that postgraphile can find yugabytedb-2 via DNS. + +Initialization: Your airflow-init container should be handled as a K8s Job or an initContainer, rather than a long-running service with a profile. + +The Result: It will generate dozens of files. Do not apply them yet. Kompose handles simple web apps well but struggles with complex distributed systems like YugabyteDB, Redpanda, or Airflow. +``` + +### Recommended Repository Structure (Gitea) +Organize your Gitea repo like this so ArgoCD can read it easily: +``` +/my-cluster-repo + /infra + nats-deployment.yaml + minio-deployment.yaml + redis-configmap.yaml + /db + yugabyte-statefulset.yaml + /streaming + redpanda-statefulset.yaml + risingwave-deployment.yaml + /apps + airflow-helm-values.yaml +``` + +### The "Missing Link": How K3s creates a cluster from images? +``` +This is the most common point of confusion. The cluster doesn't "create itself" from an image; it downloads the image to run your application. Here is the step-by-step flow: + +Gitea Action (CI): Your code changes → Gitea builds a Docker image → It pushes that image to the Gitea Container Registry (on Server A1). + +Manifest Update: The Gitea Action also updates a small YAML file in your Git repo (e.g., changing image: v1.0 to image: v1.1). + +Argo CD (CD): Argo CD (in Country B) notices the YAML file changed. + +The Pull: Argo CD tells K3s: "Update this deployment to use image v1.1." + +K3s Execution: K3s looks at the Gitea Registry in Country A, pulls (downloads) the new image, and restarts the pods. +``` + + +#### Helm Charts: K3s vs. K8s +``` +Can we use Helm for K3s? +Yes. K3s is a fully certified Kubernetes distribution. Anything that works on standard K8s works on K3s. In fact, K3s comes with a Helm Controller pre-installed, allowing you to deploy Helm charts just by dropping a YAML file into a specific folder (/var/lib/rancher/k3s/server/manifests). + +Are Helm Charts identical? +99% of the time, Yes. * The Kubernetes resources (Deployments, Services, Ingress) are identical. + +The only difference usually involves the "Ingress Controller." Standard K8s often uses NGINX, while K3s comes with Traefik by default. You might need to adjust your Helm values.yaml to tell it to use Traefik instead of NGINX. +``` +#### Helm +``` +The Kubernetes package manager + +Common actions for Helm: + +- helm search: search for charts +- helm pull: download a chart to your local directory to view +- helm install: upload the chart to Kubernetes +- helm list: list releases of charts + +Environment variables: + +| Name | Description | +| ---------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| $HELM_CACHE_HOME | set an alternative location for storing cached files. | +| $HELM_CONFIG_HOME | set an alternative location for storing Helm configuration. | +| $HELM_DATA_HOME | set an alternative location for storing Helm data. | +| $HELM_DEBUG | indicate whether or not Helm is running in Debug mode | +| $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory, sql. | +| $HELM_DRIVER_SQL_CONNECTION_STRING | set the connection string the SQL storage driver should use. | +| $HELM_MAX_HISTORY | set the maximum number of helm release history. | +| $HELM_NAMESPACE | set the namespace used for the helm operations. | +| $HELM_NO_PLUGINS | disable plugins. Set HELM_NO_PLUGINS=1 to disable plugins. | +| $HELM_PLUGINS | set the path to the plugins directory | +| $HELM_REGISTRY_CONFIG | set the path to the registry config file. | +| $HELM_REPOSITORY_CACHE | set the path to the repository cache directory | +| $HELM_REPOSITORY_CONFIG | set the path to the repositories file. | +| $KUBECONFIG | set an alternative Kubernetes configuration file (default "~/.kube/config") | +| $HELM_KUBEAPISERVER | set the Kubernetes API Server Endpoint for authentication | +| $HELM_KUBECAFILE | set the Kubernetes certificate authority file. | +| $HELM_KUBEASGROUPS | set the Groups to use for impersonation using a comma-separated list. | +| $HELM_KUBEASUSER | set the Username to impersonate for the operation. | +| $HELM_KUBECONTEXT | set the name of the kubeconfig context. | +| $HELM_KUBETOKEN | set the Bearer KubeToken used for authentication. | +| $HELM_KUBEINSECURE_SKIP_TLS_VERIFY | indicate if the Kubernetes API server's certificate validation should be skipped (insecure) | +| $HELM_KUBETLS_SERVER_NAME | set the server name used to validate the Kubernetes API server certificate | +| $HELM_BURST_LIMIT | set the default burst limit in the case the server contains many CRDs (default 100, -1 to disable) | +| $HELM_QPS | set the Queries Per Second in cases where a high number of calls exceed the option for higher burst values | +| $HELM_COLOR | set color output mode. Allowed values: never, always, auto (default: never) | +| $NO_COLOR | set to any non-empty value to disable all colored output (overrides $HELM_COLOR) | + +Helm stores cache, configuration, and data based on the following configuration order: + +- If a HELM_*_HOME environment variable is set, it will be used +- Otherwise, on systems supporting the XDG base directory specification, the XDG variables will be used +- When no other location is set a default location will be used based on the operating system + +By default, the default directories depend on the Operating System. The defaults are listed below: + +| Operating System | Cache Path | Configuration Path | Data Path | +| ---------------- | ------------------------- | ------------------------------ | ----------------------- | +| Linux | $HOME/.cache/helm | $HOME/.config/helm | $HOME/.local/share/helm | +| macOS | $HOME/Library/Caches/helm | $HOME/Library/Preferences/helm | $HOME/Library/helm | +| Windows | %TEMP%\helm | %APPDATA%\helm | %APPDATA%\helm | +``` + +``` +Usage: + helm [command] + +Available Commands: + completion generate autocompletion scripts for the specified shell + create create a new chart with the given name + dependency manage a chart's dependencies + env helm client environment information + get download extended information of a named release + help Help about any command + history fetch release history + install install a chart + lint examine a chart for possible issues + list list releases + package package a chart directory into a chart archive + plugin install, list, or uninstall Helm plugins + pull download a chart from a repository and (optionally) unpack it in local directory + push push a chart to remote + registry login to or logout from a registry + repo add, list, remove, update, and index chart repositories + rollback roll back a release to a previous revision + search search for a keyword in charts + show show information of a chart + status display the status of the named release + template locally render templates + test run tests for a release + uninstall uninstall a release + upgrade upgrade a release + verify verify that a chart at the given path has been signed and is valid + version print the helm version information + +Flags: + --burst-limit int client-side default throttling limit (default 100) + --color string use colored output (never, auto, always) (default "auto") + --colour string use colored output (never, auto, always) (default "auto") + --content-cache string path to the directory containing cached content (e.g. charts) (default "C:\\Users\\wendg2\\AppData\\Local\\Temp\\helm\\content") + --debug enable verbose output + -h, --help help for helm + --kube-apiserver string the address and the port for the Kubernetes API server + --kube-as-group stringArray group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --kube-as-user string username to impersonate for the operation + --kube-ca-file string the certificate authority file for the Kubernetes API server connection + --kube-context string name of the kubeconfig context to use + --kube-insecure-skip-tls-verify if true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kube-tls-server-name string server name to use for Kubernetes API server certificate validation. If it is not provided, the hostname used to contact the server is used + --kube-token string bearer token used for authentication + --kubeconfig string path to the kubeconfig file + -n, --namespace string namespace scope for this request + --qps float32 queries per second used when communicating with the Kubernetes API, not including bursting + --registry-config string path to the registry config file (default "C:\\Users\\wendg2\\AppData\\Roaming\\helm\\registry\\config.json") + --repository-cache string path to the directory containing cached repository indexes (default "C:\\Users\\wendg2\\AppData\\Local\\Temp\\helm\\repository") + --repository-config string path to the file containing repository names and URLs (default "C:\\Users\\wendg2\\AppData\\Roaming\\helm\\repositories.yaml") + +Use "helm [command] --help" for more information about a command. +``` + +#### What is an Ingress and when to use it? +Think of your 4 VMs as a private gated community. +``` +Pods are the houses. + +Services are the internal phone extensions (e.g., dial 4195 for Benthos). + +Ingress is the Security Guard at the Main Gate. +``` +When does it make sense? +``` +When you want "Pretty" URLs: Instead of remembering 192.168.1.50:31045, you want to type benthos.local or grafana.local. + +SSL/HTTPS: You want to handle all your security certificates in one place (the Ingress) rather than inside every single pod. + +Single Entry Point: You only want to open Port 80 (HTTP) and 443 (HTTPS) on your VM firewalls. The Ingress decides which pod gets the traffic based on the "Host" name you typed in your browser. +``` +When NOT to use it? +``` +Internal Traffic: Benthos talking to NATS should never go through an Ingress. They should talk directly via the internal Service name (nats-cluster). Ingress is for human-to-cluster or external-app-to-cluster traffic. +``` +#### How to use Ingress in K3s +K3s comes with a built-in Ingress controller called Traefik. You don't need to install anything! You just need to define an "Ingress Route." + +``` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: benthos-ingress + namespace: infra + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: web +spec: + rules: + - host: benthos.local # You will add this to your Windows 'hosts' file + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: benthos-ui + port: + number: 4195 +``` +The Final Step: The hosts file +Since benthos.local isn't a real internet address, you need to tell your Windows laptop where to find it. + +Open Notepad as Administrator. + +Open C:\Windows\System32\drivers\etc\hosts. + +Add a line with the IP of any of your 4 VMs: +192.168.x.x benthos.local + +Now, you can just type http://benthos.local in Chrome! + +#### Longhorn +Setup Storage: Install a "Storage Class" on your K3s cluster (like Longhorn or the default Local-Path Provisioner). This allows Yugabyte and ElasticSearch to claim disk space on your VMs. + + +in Docker Compose, your data stayed on the VM's local folder. In K8s, pods can "float" between VMs. If yugabytedb moves from x-vm2 to x-vm4, it needs its data to follow it. +``` +The Disaster Scenario (No Longhorn): + +Your NATS data is saved on x-vm2's hard drive. + +x-vm2 crashes or loses power. + +Kubernetes moves your NATS pod to x-vm3. + +The Problem: x-vm3 doesn't have the data! It's trapped on the dead hard drive of x-vm2. Your database is now empty or corrupted. + +The Longhorn Solution: +Longhorn takes the hard drives of all 4 VMs and creates a "Shared Pool." When NATS writes a file, Longhorn instantly copies it to 3 different VMs. If one VM dies, the pod starts on another VM, and Longhorn "plugs in" the replicated data immediately. +``` +``` +Longhorn is a lightweight storage orchestrator. It takes the empty space on all 4 of your VMs and turns it into one big "distributed disk." + +In windows: + +helm repo add longhorn https://charts.longhorn.io +helm repo update + +Install it into your cluster: +kubectl create namespace longhorn-system +helm install longhorn longhorn/longhorn --namespace longhorn-system + +kubectl patch storageclass longhorn -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}' +``` + + +### Connecting to proxmox host via ssh +1. Generate ssh key: + ssh-keygen -t ed25519 +2. Push the Key to Proxmox: + type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh root@192.168.3.50 "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys" + +pveum user token add root@pam terraform-token --privsep 0 +``` + +1. Cluster Setup (3 Nodes, 12 VMs) +First, you should join your three servers into a Proxmox Cluster. This allows you to manage all three from a single interface and move VMs between them easily. + +Create Cluster: Log into Node 1 (192.168.3.70), go to Datacenter > Cluster, and click Create Cluster. + +Join Nodes: Copy the "Join Information" and paste it into the Cluster > Join Cluster section on Node 2 and Node 3. + +VM Deployment: You can then create 4 VMs on each node. + +Pro Tip: Create one "Gold Master" VM with Docker installed, convert it to a Template, and then "Clone" it 12 times to save hours of setup. + +2. Remote Access for Developers +Since your IPs (192.x.x.x) are private, developers at other locations cannot see them. Do not use Port Forwarding for Proxmox; it is a security risk. + +The "Mesh VPN" Way (Easiest): Install Tailscale or ZeroTier on each VM (or just one "Gateway" VM). + +This creates a secure virtual network. Developers simply install the same app, and they can access the VMs as if they were in the same room. + +The Reverse Proxy Way: If the apps are web-based, use Nginx Proxy Manager or Cloudflare Tunnels. This allows developers to access apps via a URL (e.g., app1.yourdomain.com) without a VPN. + +3. CI/CD Workflow: Gitea → Docker → Argo CD +Argo CD is designed for Kubernetes, but you can use it for Docker deployments if you run a lightweight Kubernetes cluster (like K3s) inside your VMs. + +The Architecture +Gitea (The "Source"): Hosts your code and docker-compose.yaml or Kubernetes manifests. + +Gitea Actions (The "Builder"): When code is pushed, a runner builds the Docker image and pushes it to a Registry (Gitea has a built-in container registry). + +Argo CD (The "Operator"): Watches your Gitea repository. When it sees a change, it automatically pulls the new image and updates the deployment in the VM. + +Deployment Steps +Install Gitea: Run Gitea in one of your VMs (using Docker). + +Install K3s: On your 12 VMs, install K3s (curl -sfL https://get.k3s.io | sh -). This gives you a tiny Kubernetes environment in each VM. + +Install Argo CD: Install Argo CD inside your K3s cluster. + +Connect Repo: Point Argo CD to your Gitea repository URL. + +Sync: Set the sync policy to "Automatic." Now, every time a developer pushes code to Gitea, Argo CD will update the VM automatically. + +``` +free -h # physical RAM installed and recognized +df -h # Proxmox-Specific Storage +lsblk # Physical Disk Overview +fdisk -l # Detailed Hardware Info +htop # pre installed +dmidecode -t memory | grep -i size # hardware info +pveperf \ No newline at end of file diff --git a/namespaces.yaml b/namespaces.yaml new file mode 100644 index 0000000..c8240c8 --- /dev/null +++ b/namespaces.yaml @@ -0,0 +1,44 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: argocd +--- +apiVersion: v1 +kind: Namespace +metadata: + name: infra +--- +apiVersion: v1 +kind: Namespace +metadata: + name: db +--- +apiVersion: v1 +kind: Namespace +metadata: + name: stream +--- +apiVersion: v1 +kind: Namespace +metadata: + name: apps +--- +apiVersion: v1 +kind: Namespace +metadata: + name: longhorn-system +--- +apiVersion: v1 +kind: Namespace +metadata: + name: linkerd-crds +--- +apiVersion: v1 +kind: Namespace +metadata: + name: linkerd +--- +apiVersion: v1 +kind: Namespace +metadata: + name: linkerd-viz \ No newline at end of file diff --git a/namespaces/apps-ns.yaml b/namespaces/apps-ns.yaml new file mode 100644 index 0000000..01113b9 --- /dev/null +++ b/namespaces/apps-ns.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: apps \ No newline at end of file diff --git a/namespaces/argocd-ns.yaml b/namespaces/argocd-ns.yaml new file mode 100644 index 0000000..a040f2b --- /dev/null +++ b/namespaces/argocd-ns.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: argocd diff --git a/namespaces/db-ns.yaml b/namespaces/db-ns.yaml new file mode 100644 index 0000000..bab604e --- /dev/null +++ b/namespaces/db-ns.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: db diff --git a/namespaces/infra-ns.yaml b/namespaces/infra-ns.yaml new file mode 100644 index 0000000..58ff182 --- /dev/null +++ b/namespaces/infra-ns.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: infra diff --git a/namespaces/kustomization.yaml b/namespaces/kustomization.yaml new file mode 100644 index 0000000..d286db6 --- /dev/null +++ b/namespaces/kustomization.yaml @@ -0,0 +1,10 @@ +# namespaces/kustomization.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - apps-ns.yaml + - argocd-ns.yaml + - db-ns.yaml + - infra-ns.yaml + - stream-ns.yaml \ No newline at end of file diff --git a/namespaces/stream-ns.yaml b/namespaces/stream-ns.yaml new file mode 100644 index 0000000..72172c3 --- /dev/null +++ b/namespaces/stream-ns.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: stream diff --git a/setup-local-tools.yml b/setup-local-tools.yml new file mode 100644 index 0000000..7187bce --- /dev/null +++ b/setup-local-tools.yml @@ -0,0 +1,45 @@ +--- +- name: Install Kubernetes Tools on WSL Localhost + hosts: localhost + connection: local + become: yes + tasks: + - name: Install dependencies + ansible.builtin.apt: + name: [curl, git, gpg] + state: present + update_cache: yes + + - name: Install Helm via Official Script + ansible.builtin.shell: | + curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + args: + creates: /usr/local/bin/helm + + - name: Install Kustomize via Official Installer + ansible.builtin.shell: | + curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash + mv kustomize /usr/local/bin/kustomize + args: + creates: /usr/local/bin/kustomize + + - name: Ensure binaries are executable + ansible.builtin.file: + path: "{{ item }}" + mode: '0755' + owner: root + group: root + loop: + - /usr/local/bin/helm + - /usr/local/bin/kustomize + + - name: Verify Installations + ansible.builtin.shell: "{{ item }} version" + loop: [helm, kustomize] + register: tool_versions + changed_when: false + + - name: Print Versions + ansible.builtin.debug: + msg: "{{ item.stdout }}" + loop: "{{ tool_versions.results }}" \ No newline at end of file diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/.helmignore b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/.helmignore new file mode 100644 index 0000000..3598c30 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/.helmignore @@ -0,0 +1 @@ +tests \ No newline at end of file diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/Chart.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/Chart.yaml new file mode 100644 index 0000000..3f2f263 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/Chart.yaml @@ -0,0 +1,18 @@ +annotations: + charts.openshift.io/name: yugabyte +apiVersion: v2 +appVersion: 2025.2.1.0-b141 +description: YugabyteDB is the high-performance distributed SQL database for building + global, internet-scale apps. +home: https://www.yugabyte.com +icon: https://avatars0.githubusercontent.com/u/17074854?s=200&v=4 +kubeVersion: '>=1.17.0-0' +maintainers: +- email: sanketh@yugabyte.com + name: Sanketh Indarapu +- email: gjalla@yugabyte.com + name: Govardhan Reddy Jalla +name: yugabyte +sources: +- https://github.com/yugabyte/yugabyte-db +version: 2025.2.1 diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/README.md b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/README.md new file mode 100644 index 0000000..9528cff --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/README.md @@ -0,0 +1 @@ +YugabyteDB can be deployed in various Kubernetes configurations (including single zone, multi-zone and multi-cluster) using this Helm Chart. Detailed documentation is available in [YugabyteDB Docs for Kubernetes Deployments](https://docs.yugabyte.com/latest/deploy/kubernetes/). diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/app-readme.md b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/app-readme.md new file mode 100644 index 0000000..dcd1192 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/app-readme.md @@ -0,0 +1 @@ +This chart bootstraps an RF3 YugabyteDB version 2025.2.1.0-b141 cluster using the Helm Package Manager. diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/expose-all-shared.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/expose-all-shared.yaml new file mode 100644 index 0000000..fae0234 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/expose-all-shared.yaml @@ -0,0 +1,21 @@ +serviceEndpoints: + - name: "yb-master-service" + type: LoadBalancer + app: "yb-master" + ports: + ui: "7000" + + - name: "yb-tserver-service" + type: LoadBalancer + app: "yb-tserver" + ports: + yql-port: "9042" + yedis-port: "6379" + ysql-port: "5433" + + - name: "yugabyted-ui-service" + type: LoadBalancer + app: "yb-master" + sessionAffinity: ClientIP + ports: + yugabyted-ui: "15433" diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/expose-all.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/expose-all.yaml new file mode 100644 index 0000000..ac3edf5 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/expose-all.yaml @@ -0,0 +1,31 @@ +serviceEndpoints: + - name: "yb-master-ui" + type: LoadBalancer + app: "yb-master" + ports: + ui: "7000" + + - name: "yql-service" + type: LoadBalancer + app: "yb-tserver" + ports: + yql-port: "9042" + + - name: "yedis-service" + type: LoadBalancer + app: "yb-tserver" + ports: + yedis-port: "6379" + + - name: "ysql-service" + type: LoadBalancer + app: "yb-tserver" + ports: + ysql-port: "5433" + + - name: "yugabyted-ui-service" + type: LoadBalancer + app: "yb-master" + sessionAffinity: ClientIP + ports: + yugabyted-ui: "15433" diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/generate_kubeconfig.py b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/generate_kubeconfig.py new file mode 100644 index 0000000..0d91739 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/generate_kubeconfig.py @@ -0,0 +1,220 @@ +#!/usr/bin/python +# Copyright (c) YugaByte, Inc. + +# This script would generate a kubeconfig for the given servie account +# by fetching the cluster information and also add the service account +# token for the authentication purpose. + +import argparse +from subprocess import check_output +from sys import exit +import json +import base64 +import tempfile +import time +import os.path + + +def run_command(command_args, namespace=None, as_json=True, log_command=True): + command = ["kubectl"] + if namespace: + command.extend(["--namespace", namespace]) + command.extend(command_args) + if as_json: + command.extend(["-o", "json"]) + if log_command: + print("Running command: {}".format(" ".join(command))) + output = check_output(command) + if as_json: + return json.loads(output) + else: + return output.decode("utf8") + + +def create_sa_token_secret(directory, sa_name, namespace): + """Creates a service account token secret for sa_name in + namespace. Returns the name of the secret created. + + Ref: + https://k8s.io/docs/concepts/configuration/secret/#service-account-token-secrets + + """ + token_secret = { + "apiVersion": "v1", + "data": { + "do-not-delete-used-for-yugabyte-anywhere": "MQ==", + }, + "kind": "Secret", + "metadata": { + "annotations": { + "kubernetes.io/service-account.name": sa_name, + }, + "name": sa_name, + }, + "type": "kubernetes.io/service-account-token", + } + token_secret_file_name = os.path.join(directory, "token_secret.yaml") + with open(token_secret_file_name, "w") as token_secret_file: + json.dump(token_secret, token_secret_file) + run_command(["apply", "-f", token_secret_file_name], namespace) + return sa_name + + +def get_secret_data(secret, namespace): + """Returns the secret in JSON format if it has ca.crt and token in + it, else returns None. It retries 3 times with 1 second timeout + for the secret to be populated with this data. + + """ + secret_data = None + num_retries = 5 + timeout = 2 + while True: + secret_json = run_command(["get", "secret", secret], namespace) + if "ca.crt" in secret_json["data"] and "token" in secret_json["data"]: + secret_data = secret_json + break + + num_retries -= 1 + if num_retries == 0: + break + print( + "Secret '{}' is not populated. Sleep {}s, ({} retries left)".format( + secret, timeout, num_retries + ) + ) + time.sleep(timeout) + return secret_data + + +def get_secrets_for_sa(sa_name, namespace): + """Returns a list of all service account token secrets associated + with the given sa_name in the namespace. + + """ + secrets = run_command( + [ + "get", + "secret", + "--field-selector", + "type=kubernetes.io/service-account-token", + "-o", + 'jsonpath="{.items[?(@.metadata.annotations.kubernetes\.io/service-account\.name == "' + + sa_name + + '")].metadata.name}"', + ], + namespace, + as_json=False, + ) + return secrets.strip('"').split() + + +parser = argparse.ArgumentParser(description="Generate KubeConfig with Token") +parser.add_argument("-s", "--service_account", help="Service Account name", required=True) +parser.add_argument("-n", "--namespace", help="Kubernetes namespace", default="kube-system") +parser.add_argument("-c", "--context", help="kubectl context") +parser.add_argument("-o", "--output_file", help="output file path") +args = vars(parser.parse_args()) + +# if the context is not provided we use the current-context +context = args["context"] +if context is None: + context = run_command(["config", "current-context"], args["namespace"], as_json=False) + +cluster_attrs = run_command( + ["config", "get-contexts", context.strip(), "--no-headers"], args["namespace"], as_json=False +) + +cluster_name = cluster_attrs.strip().split()[2] +endpoint = run_command( + [ + "config", + "view", + "-o", + 'jsonpath="{.clusters[?(@.name =="' + cluster_name + '")].cluster.server}"', + ], + args["namespace"], + as_json=False, +) +service_account_info = run_command(["get", "sa", args["service_account"]], args["namespace"]) + +tmpdir = tempfile.TemporaryDirectory() + +# Get the token and ca.crt from service account secret. +sa_secrets = list() + +# Get secrets specified in the service account, there can be multiple +# of them, and not all are service account token secrets. +if "secrets" in service_account_info: + sa_secrets = [secret["name"] for secret in service_account_info["secrets"]] + +# Find the existing additional service account token secrets +sa_secrets.extend(get_secrets_for_sa(args["service_account"], args["namespace"])) + +secret_data = None +for secret in sa_secrets: + secret_data = get_secret_data(secret, args["namespace"]) + if secret_data is not None: + break + +# Kubernetes 1.22+ doesn't create the service account token secret by +# default, we have to create one. +if secret_data is None: + print("No usable secret found for '{}', creating one.".format(args["service_account"])) + token_secret = create_sa_token_secret(tmpdir.name, args["service_account"], args["namespace"]) + secret_data = get_secret_data(token_secret, args["namespace"]) + if secret_data is None: + exit( + "Failed to generate kubeconfig: No usable credentials found for '{}'.".format( + args["service_account"] + ) + ) + + +context_name = "{}-{}".format(args["service_account"], cluster_name) +kube_config = args["output_file"] +if not kube_config: + kube_config = "/tmp/{}.conf".format(args["service_account"]) + + +ca_crt_file_name = os.path.join(tmpdir.name, "ca.crt") +ca_crt_file = open(ca_crt_file_name, "wb") +ca_crt_file.write(base64.b64decode(secret_data["data"]["ca.crt"])) +ca_crt_file.close() + +# create kubeconfig entry +set_cluster_cmd = [ + "config", + "set-cluster", + cluster_name, + "--kubeconfig={}".format(kube_config), + "--server={}".format(endpoint.strip('"')), + "--embed-certs=true", + "--certificate-authority={}".format(ca_crt_file_name), +] +run_command(set_cluster_cmd, as_json=False) + +user_token = base64.b64decode(secret_data["data"]["token"]).decode("utf-8") +set_credentials_cmd = [ + "config", + "set-credentials", + context_name, + "--token={}".format(user_token), + "--kubeconfig={}".format(kube_config), +] +run_command(set_credentials_cmd, as_json=False, log_command=False) + +set_context_cmd = [ + "config", + "set-context", + context_name, + "--cluster={}".format(cluster_name), + "--user={}".format(context_name), + "--kubeconfig={}".format(kube_config), +] +run_command(set_context_cmd, as_json=False) + +use_context_cmd = ["config", "use-context", context_name, "--kubeconfig={}".format(kube_config)] +run_command(use_context_cmd, as_json=False) + +print("Generated the kubeconfig file: {}".format(kube_config)) diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/openshift.values.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/openshift.values.yaml new file mode 100644 index 0000000..d2784b2 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/openshift.values.yaml @@ -0,0 +1,4 @@ +# OCP compatible values for yugabyte + +Image: + repository: "quay.io/yugabyte/yugabyte-ubi" diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/questions.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/questions.yaml new file mode 100644 index 0000000..6befa49 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/questions.yaml @@ -0,0 +1,174 @@ +--- +questions: + ## Default images for yugabyte + - variable: questions.defaultYugabyteDB + default: true + description: "Use default settings for YugabyteDB" + label: Use default + type: boolean + show_subquestion_if: false + group: "YugabyteDB" + subquestions: + - variable: Image.repository + default: "yugabytedb/yugabyte" + required: true + type: string + label: YugabyteDB image repository + description: "YugabyteDB image repository" + - variable: Image.tag + default: "2.5.1.0-b153" + required: true + type: string + label: YugabyteDB image tag + description: "YugabyteDB image tag" + - variable: Image.pullPolicy + default: "IfNotPresent" + required: false + type: enum + options: + - "Always" + - "IfNotPresent" + label: YugabyteDB image pull policy + description: "YugabyteDB image pull policy" + - variable: storage.ephemeral + default: false + required: false + type: boolean + label: YugabyteDB ephemeral storage + description: "Won't allocate PVs when true" + - variable: replicas.master + default: 3 + description: "Number of replicas for Master" + type: int + required: true + label: Replication Factor Master + - variable: replicas.tserver + default: 3 + description: "Number of replicas for TServer" + type: int + required: true + label: Replication Factor TServer + - variable: statefulSetAnnotations + description: Annotations for the StatefulSet + type: dict + required: false + label: "Annotations for the StatefulSet" + - variable: questions.defaultMasterStorage + default: true + description: "Use default storage configurations for YugabyteDB Master" + label: Use default storage configurations + type: boolean + show_subquestion_if: false + group: "Master Storage" + subquestions: + - variable: storage.master.count + default: 2 + required: true + type: int + label: YugabyteDB master storage disk count + description: "YugabyteDB master storage disk count" + - variable: storage.master.size + default: "10Gi" + required: true + type: string + label: YugabyteDB master storage size + description: "YugabyteDB master storage size" + - variable: storage.master.storageClass + default: "" + required: false + type: storageclass + label: YugabyteDB master storage class + description: "YugabyteDB master storage class" + - variable: questions.defaultTServerStorage + default: true + description: "Use default storage configurations for YugabyteDB TServer" + label: Use default storage configuration + type: boolean + show_subquestion_if: false + group: "TServer Storage" + subquestions: + - variable: storage.tserver.count + default: 2 + required: true + type: int + label: YugabyteDB TServer storage disk count + description: "YugabyteDB TServer storage disk count" + - variable: storage.tserver.size + default: "10Gi" + required: true + type: string + label: YugabyteDB TServer storage size + description: "YugabyteDB TServer storage size" + - variable: storage.tserver.storageClass + default: "" + required: false + type: storageclass + label: YugabyteDB TServer storage class + description: "YugabyteDB TServer storage class" + ## Default resources + - variable: questions.defaultResources + default: true + description: "Use default resources for YugabyteDB" + label: Use default resources + type: boolean + show_subquestion_if: false + group: "Resources" + subquestions: + - variable: resource.master.requests.cpu + default: "2" + description: "Master vcpu allocation for YugabyteDB" + type: string + required: true + label: vcpu allocation for master + - variable: resource.master.requests.memory + default: "2Gi" + description: "Master RAM allocation for YugabyteDB" + type: string + required: true + label: RAM allocation for master + - variable: resource.tserver.requests.cpu + default: "2" + description: "TServer vcpu allocation for YugabyteDB" + type: string + required: true + label: vcpu allocation for tserver + - variable: resource.tserver.requests.memory + default: "4Gi" + description: "TServer RAM allocation for YugabyteDB" + type: string + required: true + label: RAM allocation for tserver + ## TLS + - variable: tls.enabled + default: false + description: "Enable TLS - TLS disabled by default" + label: Enable TLS + type: boolean + show_subquestion_if: true + group: "TLS" + subquestions: + - variable: tls.nodeToNode + default: true + description: "Node to Node" + type: boolean + required: false + label: Node to Node + - variable: tls.clientToServer + default: true + description: "Client to server" + type: boolean + required: false + label: Client to server + - variable: tls.insecure + default: false + description: "Insecure - no service will connect on unencrypted connection" + type: boolean + required: false + label: Insecure communication + - variable: tls.certManager.enabled + default: false + description: "Use cert-manager to provide cluster certificates" + type: boolean + required: false + label: Cert-Manager Support + diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/NOTES.txt b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/NOTES.txt new file mode 100644 index 0000000..2f90d74 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/NOTES.txt @@ -0,0 +1,29 @@ +1. Get YugabyteDB Pods by running this command: + kubectl --namespace {{ .Release.Namespace }} get pods + +2. Get list of YugabyteDB services that are running: + kubectl --namespace {{ .Release.Namespace }} get services + +3. Get information about the load balancer services: + kubectl get svc --namespace {{ .Release.Namespace }} + +4. Connect to one of the tablet server: + kubectl exec --namespace {{ .Release.Namespace }} -it yb-tserver-0 bash + +5. Run YSQL shell from inside of a tablet server: + kubectl exec --namespace {{ .Release.Namespace }} -it yb-tserver-0 -- /home/yugabyte/bin/ysqlsh -h yb-tserver-0.yb-tservers.{{ .Release.Namespace }} + +6. Cleanup YugabyteDB Pods + For helm 2: + helm delete {{ .Release.Name }} --purge + For helm 3: + helm delete {{ .Release.Name }} -n {{ .Release.Namespace }} + NOTE: You need to manually delete the persistent volume + {{- $root := . -}} + {{- range .Values.Services }} + kubectl delete pvc --namespace {{ $root.Release.Namespace }} -l app={{.label}} + {{- end }} +{{ if $root.Values.yugabytedUi.enabled }} +NOTE: The yugabyted UI is now available and is enabled by default. It requires version 2.21.0 or greater. +If you are using a custom image of YugabyteDB that is older than 2.21.0, please disable the UI by setting yugabytedUi.enabled to false. +{{- end }} diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/_helpers.tpl b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/_helpers.tpl new file mode 100644 index 0000000..95cfba8 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/_helpers.tpl @@ -0,0 +1,674 @@ +{{/* vim: set filetype=mustache: */}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +The components in this chart create additional resources that expand the longest created name strings. +The longest name that gets created of 20 characters, so truncation should be 63-20=43. +*/}} +{{- define "yugabyte.fullname" -}} + {{- if .Values.fullnameOverride -}} + {{- .Values.fullnameOverride | trunc 43 | trimSuffix "-" -}} + {{- else -}} + {{- $name := default .Chart.Name .Values.nameOverride -}} + {{- if contains $name .Release.Name -}} + {{- .Release.Name | trunc 43 | trimSuffix "-" -}} + {{- else -}} + {{- printf "%s-%s" .Release.Name $name | trunc 43 | trimSuffix "-" -}} + {{- end -}} + {{- end -}} +{{- end -}} + +{{/* +Generate common labels. +*/}} +{{- define "yugabyte.labels" }} +heritage: {{ .Values.helm2Legacy | ternary "Tiller" (.Release.Service | quote) }} +release: {{ .Release.Name | quote }} +chart: {{ .Chart.Name | quote }} +component: {{ .Values.Component | quote }} +{{- if .Values.commonLabels}} +{{ toYaml .Values.commonLabels }} +{{- end }} +{{- end }} + +{{/* +Generate app label. +*/}} +{{- define "yugabyte.applabel" }} +{{- if .root.Values.oldNamingStyle }} +app: "{{ .label }}" +{{- else }} +app.kubernetes.io/name: "{{ .label }}" +{{- end }} +{{- end }} + +{{/* +Generate app selector. +*/}} +{{- define "yugabyte.appselector" }} +{{- if .root.Values.oldNamingStyle }} +app: "{{ .label }}" +{{- else }} +app.kubernetes.io/name: "{{ .label }}" +release: {{ .root.Release.Name | quote }} +{{- end }} +{{- end }} + +{{/* +Generate service name. +*/}} +{{- define "yugabyte.servicename" }} + {{- if eq .scope "Namespaced" }} + {{- $prefix := (get (.root.Values.commonLabels | default dict) "app.kubernetes.io/part-of" | default "namespaced") | trunc 43 | trimSuffix "-" }} + {{- if $prefix }} + {{- printf "%s-%s" $prefix .endpoint.name }} + {{- else }} + {{- .endpoint.name }} + {{- end }} + {{- else }} + {{- .root.Values.oldNamingStyle | ternary .endpoint.name (printf "%s-%s" (include "yugabyte.fullname" $.root) .endpoint.name) }} + {{- end }} +{{- end }} + +{{/* +Get service scope +*/}} +{{- define "yugabyte.servicescope" }} + {{- if .endpoint.scope }} + {{- .endpoint.scope }} + {{- else }} + {{- .defaultScope }} + {{- end }} +{{- end }} + +{{/* +Generate namespaced service selector. +*/}} +{{- define "yugabyte.namespacedserviceselector" }} +app.kubernetes.io/name: "{{ .label }}" +{{- $partof := (get (.root.Values.commonLabels | default dict) "app.kubernetes.io/part-of" | default "")}} +{{- if $partof }} +app.kubernetes.io/part-of: "{{ $partof }}" +{{- end }} +{{- end }} + +{{/* +Checks if a service is required to be installed/upgraded +*/}} +{{- define "yugabyte.should_render_service" -}} + {{- if eq .scope "AZ" }} + {{- "true" }} + {{- else }} + {{- $namespacedService := (lookup "v1" "Service" .root.Release.Namespace .serviceName) }} + {{- if not $namespacedService }} + {{- "true" }} + {{- else }} + {{- $ownerRelease := (get $namespacedService.metadata.annotations "meta.helm.sh/release-name") | default "" }} + {{- if eq $ownerRelease .root.Release.Name }} + {{- "true" }} + {{- else }} + {{- "false" }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} + +{{/* +Create secrets in DBNamespace from other namespaces by iterating over envSecrets. +*/}} +{{- define "yugabyte.envsecrets" -}} +{{- range $v := .secretenv }} +{{- if $v.valueFrom.secretKeyRef.namespace }} +{{- $secretObj := (lookup +"v1" +"Secret" +$v.valueFrom.secretKeyRef.namespace +$v.valueFrom.secretKeyRef.name) +| default dict }} +{{- $secretData := (get $secretObj "data") | default dict }} +{{- $secretValue := (get $secretData $v.valueFrom.secretKeyRef.key) | default "" }} +{{- if (and (not $secretValue) (not $v.valueFrom.secretKeyRef.optional)) }} +{{- required (printf "Secret or key missing for %s/%s in namespace: %s" +$v.valueFrom.secretKeyRef.name +$v.valueFrom.secretKeyRef.key +$v.valueFrom.secretKeyRef.namespace) +nil }} +{{- end }} +{{- if $secretValue }} +apiVersion: v1 +kind: Secret +metadata: + {{- $secretfullname := printf "%s-%s-%s-%s" + $.root.Release.Name + $v.valueFrom.secretKeyRef.namespace + $v.valueFrom.secretKeyRef.name + $v.valueFrom.secretKeyRef.key + }} + name: {{ printf "%s-%s-%s-%s-%s-%s" + $.root.Release.Name + ($v.valueFrom.secretKeyRef.namespace | substr 0 5) + ($v.valueFrom.secretKeyRef.name | substr 0 5) + ( $v.valueFrom.secretKeyRef.key | substr 0 5) + (sha256sum $secretfullname | substr 0 4) + ($.suffix) + | lower | replace "." "" | replace "_" "" + }} + namespace: "{{ $.root.Release.Namespace }}" + labels: + {{- include "yugabyte.labels" $.root | indent 4 }} +type: Opaque # should it be an Opaque secret? +data: + {{ $v.valueFrom.secretKeyRef.key }}: {{ $secretValue | quote }} +{{- end }} +{{- end }} +--- +{{- end }} +{{- end }} + +{{/* +Add env secrets to DB statefulset. +*/}} +{{- define "yugabyte.addenvsecrets" -}} +{{- range $v := .secretenv }} +- name: {{ $v.name }} + valueFrom: + secretKeyRef: + {{- if $v.valueFrom.secretKeyRef.namespace }} + {{- $secretfullname := printf "%s-%s-%s-%s" + $.root.Release.Name + $v.valueFrom.secretKeyRef.namespace + $v.valueFrom.secretKeyRef.name + $v.valueFrom.secretKeyRef.key + }} + name: {{ printf "%s-%s-%s-%s-%s-%s" + $.root.Release.Name + ($v.valueFrom.secretKeyRef.namespace | substr 0 5) + ($v.valueFrom.secretKeyRef.name | substr 0 5) + ($v.valueFrom.secretKeyRef.key | substr 0 5) + (sha256sum $secretfullname | substr 0 4) + ($.suffix) + | lower | replace "." "" | replace "_" "" + }} + {{- else }} + name: {{ $v.valueFrom.secretKeyRef.name }} + {{- end }} + key: {{ $v.valueFrom.secretKeyRef.key }} + optional: {{ $v.valueFrom.secretKeyRef.optional | default "false" }} +{{- end }} +{{- end }} +{{/* +Create Volume name. +*/}} +{{- define "yugabyte.volume_name" -}} + {{- printf "%s-datadir" (include "yugabyte.fullname" .) -}} +{{- end -}} + +{{/* +Derive the memory hard limit in bytes for Master and Tserver components based on +a given memory size and a limit percentage. + +The function expects two parameters: +1. 'size': Specifies memory in 'G' or 'Gi' format (e.g., "2Gi"). +2. 'limitPercent': An integer representing the percentage of the memory limit (e.g., 85 for 85%). + +It uses a base multiplier of 1000 for 'G' units and 1024 for 'Gi' units. +*/}} +{{- define "yugabyte.memory_hard_limit" -}} + {{- $baseMultiplier := 1000 -}} + {{- if .size | toString | hasSuffix "Gi" -}} + {{- $baseMultiplier = 1024 -}} + {{- end -}} + {{- $limit_percent := .limitPercent -}} + {{- $multiplier := int (div (mul $limit_percent $baseMultiplier) 100) -}} + {{- printf "%d" .size | regexFind "\\d+" | mul $baseMultiplier | mul $baseMultiplier | mul $multiplier -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "yugabyte.chart" -}} + {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Generate a preflight check script invocation. +*/}} +{{- define "yugabyte.preflight_check" -}} +{{- if not .Values.preflight.skipAll -}} +{{- $port := .Preflight.Port -}} +{{- range $addr := split "," .Preflight.Addr -}} +if [ -f /home/yugabyte/tools/k8s_preflight.py ]; then + PYTHONUNBUFFERED="true" /home/yugabyte/tools/k8s_preflight.py \ + dnscheck \ + --addr="{{ $addr }}" \ +{{- if not $.Values.preflight.skipBind }} + --port="{{ $port }}" +{{- else }} + --skip_bind +{{- end }} +fi && \ +{{ end }} +{{- end }} +{{- end }} + +{{/* +Get YugaByte fs data directories. +*/}} +{{- define "yugabyte.fs_data_dirs" -}} + {{- range $index := until (int (.count)) -}} + {{- if ne $index 0 }},{{ end }}/mnt/disk{{ $index -}} + {{- end -}} +{{- end -}} + +{{/* +Get files from fs data directories for readiness / liveness probes. +*/}} +{{- define "yugabyte.fs_data_dirs_probe_files" -}} + {{- range $index := until (int (.count)) -}} + {{- if ne $index 0 }} {{ end }}"/mnt/disk{{ $index -}}/disk.check" + {{- end -}} +{{- end -}} + + +{{/* +Command to do a disk write and sync for liveness probes. +*/}} +{{- define "yugabyte.fs_data_dirs_probe" -}} +echo "disk check at: $(date)" \ + | tee {{ template "yugabyte.fs_data_dirs_probe_files" . }} \ + && sync {{ template "yugabyte.fs_data_dirs_probe_files" . }} +{{- end -}} + + +{{/* +Generate server FQDN. +*/}} +{{- define "yugabyte.server_fqdn" -}} + {{- if .Values.multicluster.createServicePerPod -}} + {{- printf "${HOSTNAME}.${NAMESPACE}.svc.%s" .Values.domainName -}} + {{- else if (and .Values.oldNamingStyle .Values.multicluster.createServiceExports) -}} + {{ $membershipName := required "A valid membership name is required! Please set multicluster.kubernetesClusterId" .Values.multicluster.kubernetesClusterId }} + {{- printf "${HOSTNAME}.%s.%s.${NAMESPACE}.svc.clusterset.local" $membershipName .Service.name -}} + {{- else if .Values.oldNamingStyle -}} + {{- printf "${HOSTNAME}.%s.${NAMESPACE}.svc.%s" .Service.name .Values.domainName -}} + {{- else -}} + {{- if .Values.multicluster.createServiceExports -}} + {{ $membershipName := required "A valid membership name is required! Please set multicluster.kubernetesClusterId" .Values.multicluster.kubernetesClusterId }} + {{- printf "${HOSTNAME}.%s.%s-%s.${NAMESPACE}.svc.clusterset.local" $membershipName (include "yugabyte.fullname" .) .Service.name -}} + {{- else -}} + {{- printf "${HOSTNAME}.%s-%s.${NAMESPACE}.svc.%s" (include "yugabyte.fullname" .) .Service.name .Values.domainName -}} + {{- end -}} + {{- end -}} +{{- end -}} + +{{/* +Generate server broadcast address. +*/}} +{{- define "yugabyte.server_broadcast_address" -}} + {{- include "yugabyte.server_fqdn" . }}:{{ index .Service.ports "tcp-rpc-port" -}} +{{- end -}} + +{{/* +Generate server RPC bind address. + +In case of multi-cluster services (MCS), we set it to ${POD_IP} to +ensure YCQL uses a resolvable address. +See https://github.com/yugabyte/yugabyte-db/issues/16155 + +We use a workaround for above in case of Istio by setting it to +${POD_IP} and localhost. Master doesn't support that combination, so +we stick to 0.0.0.0, which works for master. +*/}} +{{- define "yugabyte.rpc_bind_address" -}} + {{- $port := index .Service.ports "tcp-rpc-port" -}} + {{- if .Values.istioCompatibility.enabled -}} + {{- if (eq .Service.name "yb-masters") -}} + 0.0.0.0:{{ $port }} + {{- else -}} + ${POD_IP}:{{ $port }},127.0.0.1:{{ $port }} + {{- end -}} + {{- else if (or .Values.multicluster.createServiceExports .Values.multicluster.createServicePerPod) -}} + ${POD_IP}:{{ $port }} + {{- else -}} + {{- include "yugabyte.server_fqdn" . -}} + {{- end -}} +{{- end -}} + +{{/* +Generate server web interface. +*/}} +{{- define "yugabyte.webserver_interface" -}} + {{- eq .Values.ip_version_support "v6_only" | ternary "[::]" "0.0.0.0" -}} +{{- end -}} + +{{/* +Generate server CQL proxy bind address. +*/}} +{{- define "yugabyte.cql_proxy_bind_address" -}} + {{- eq .Values.ip_version_support "v6_only" | ternary "[::]" "0.0.0.0" -}}:{{ index .Service.ports "tcp-yql-port" -}} +{{- end -}} + +{{/* +Generate server PGSQL proxy bind address. +*/}} +{{- define "yugabyte.pgsql_proxy_bind_address" -}} + {{- eq .Values.ip_version_support "v6_only" | ternary "[::]" "0.0.0.0" -}}:{{ index .Service.ports "tcp-ysql-port" -}} +{{- end -}} + +{{/* +Get YugaByte master addresses +*/}} +{{- define "yugabyte.master_addresses" -}} + {{- $master_replicas := .Values.replicas.master | int -}} + {{- $domain_name := .Values.domainName -}} + {{- $newNamingStylePrefix := printf "%s-" (include "yugabyte.fullname" .) -}} + {{- $prefix := ternary "" $newNamingStylePrefix $.Values.oldNamingStyle -}} + {{- range .Values.Services -}} + {{- if eq .name "yb-masters" -}} + {{- range $index := until $master_replicas -}} + {{- if ne $index 0 }},{{ end -}} + {{- $prefix }}yb-master-{{ $index }}.{{ $prefix }}yb-masters.${NAMESPACE}.svc.{{ $domain_name }}:7100 + {{- end -}} + {{- end -}} + {{- end -}} +{{- end -}} + +{{/* +Compute the maximum number of unavailable pods based on the number of master replicas +*/}} +{{- define "yugabyte.max_unavailable_for_quorum" -}} + {{- $master_replicas_100x := .Values.replicas.master | int | mul 100 -}} + {{- $max_unavailable_master_replicas := 100 | div (100 | sub (2 | div ($master_replicas_100x | add 100))) -}} + {{- printf "%d" $max_unavailable_master_replicas -}} +{{- end -}} + +{{/* +Set consistent issuer name. +*/}} +{{- define "yugabyte.tls_cm_issuer" -}} + {{- if .Values.tls.certManager.bootstrapSelfsigned -}} + {{ .Values.oldNamingStyle | ternary "yugabyte-selfsigned" (printf "%s-selfsigned" (include "yugabyte.fullname" .)) }} + {{- else -}} + {{- if .Values.tls.certManager.useCustomIssuer -}} + {{ .Values.tls.certManager.customIssuer.name }} + {{- else -}} + {{ .Values.tls.certManager.useClusterIssuer | ternary .Values.tls.certManager.clusterIssuer .Values.tls.certManager.issuer }} + {{- end -}} + {{- end -}} +{{- end -}} + +{{/* +Set issuer kind. +*/}} +{{- define "yugabyte.tls_issuer_kind" -}} + {{- if .Values.tls.certManager.useCustomIssuer -}} + {{ .Values.tls.certManager.customIssuer.kind }} + {{- else -}} + {{ .Values.tls.certManager.useClusterIssuer | ternary "ClusterIssuer" "Issuer" }} + {{- end -}} +{{- end -}} + +{{/* +Set issuer group. +*/}} +{{- define "yugabyte.tls_issuer_group" -}} + {{- if .Values.tls.certManager.useCustomIssuer -}} + {{ .Values.tls.certManager.customIssuer.group | default "cert-manager.io" }} + {{- else -}} + {{ "" }} + {{- end -}} +{{- end -}} + +{{/* + Verify the extraVolumes and extraVolumeMounts mappings. + Every extraVolumes should have extraVolumeMounts +*/}} +{{- define "yugabyte.isExtraVolumesMappingExists" -}} + {{- $lenExtraVolumes := len .extraVolumes -}} + {{- $lenExtraVolumeMounts := len .extraVolumeMounts -}} + + {{- if and (eq $lenExtraVolumeMounts 0) (gt $lenExtraVolumes 0) -}} + {{- fail "You have not provided the extraVolumeMounts for extraVolumes." -}} + {{- else if and (eq $lenExtraVolumes 0) (gt $lenExtraVolumeMounts 0) -}} + {{- fail "You have not provided the extraVolumes for extraVolumeMounts." -}} + {{- else if and (gt $lenExtraVolumes 0) (gt $lenExtraVolumeMounts 0) -}} + {{- $volumeMountsList := list -}} + {{- range .extraVolumeMounts -}} + {{- $volumeMountsList = append $volumeMountsList .name -}} + {{- end -}} + + {{- $volumesList := list -}} + {{- range .extraVolumes -}} + {{- $volumesList = append $volumesList .name -}} + {{- end -}} + + {{- range $volumesList -}} + {{- if not (has . $volumeMountsList) -}} + {{- fail (printf "You have not provided the extraVolumeMounts for extraVolume %s" .) -}} + {{- end -}} + {{- end -}} + + {{- range $volumeMountsList -}} + {{- if not (has . $volumesList) -}} + {{- fail (printf "You have not provided the extraVolumes for extraVolumeMounts %s" .) -}} + {{- end -}} + {{- end -}} + {{- end -}} +{{- end -}} + +{{/* + Default nodeAffinity for multi-az deployments +*/}} +{{- define "yugabyte.multiAZNodeAffinity" -}} +requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: failure-domain.beta.kubernetes.io/zone + operator: In + values: + - {{ quote .Values.AZ }} + - matchExpressions: + - key: topology.kubernetes.io/zone + operator: In + values: + - {{ quote .Values.AZ }} +{{- end -}} + +{{/* + Default podAntiAffinity for master and tserver + + This requires "appLabelArgs" to be passed in - defined in service.yaml + we have a .root and a .label in appLabelArgs +*/}} +{{- define "yugabyte.podAntiAffinity" -}} +preferredDuringSchedulingIgnoredDuringExecution: +- weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + {{- if .root.Values.oldNamingStyle }} + - key: app + operator: In + values: + - "{{ .label }}" + {{- else }} + - key: app.kubernetes.io/name + operator: In + values: + - "{{ .label }}" + - key: release + operator: In + values: + - {{ .root.Release.Name | quote }} + {{- end }} + topologyKey: kubernetes.io/hostname +{{- end -}} + +{{/* + YB Master ports +*/}} +{{- define "yugabyte.yb_masters.ports" -}} +{{- $masterPorts := dict -}} +{{- range .Values.Services -}} + {{- if eq .name "yb-masters" -}} + {{- range $key, $value := .ports -}} + {{- $masterPorts = set $masterPorts $key $value -}} + {{- end -}} + {{- end -}} +{{- end -}} +{{- toYaml $masterPorts -}} +{{- end -}} + +{{/* + Readiness Probe for Master +*/}} +{{- define "yugabyte.master.readinessProbe" -}} +{{- if .Values.master.customReadinessProbe -}} +{{- toYaml .Values.master.customReadinessProbe }} +{{- else if .Values.master.readinessProbe.enabled -}} +{{- toYaml (omit .Values.master.readinessProbe "enabled") }} +httpGet: + path: / + port: {{ index (include "yugabyte.yb_masters.ports" .| fromYaml) "http-ui" }} +{{- end -}} +{{- end -}} + +{{/* + YB Tservers ports +*/}} +{{- define "yugabyte.yb_tservers.ports" -}} +{{- $tserverPorts := dict -}} +{{- range .Values.Services }} + {{- if eq .name "yb-tservers" }} + {{- range $key, $value := .ports }} + {{- $tserverPorts = set $tserverPorts $key $value }} + {{- end }} + {{- end }} +{{- end }} +{{- toYaml $tserverPorts -}} +{{- end -}} + +{{/* + Readiness Probe for Tserver + Use ".Values.authCredentials.ysql.password" while setting ysql credentials through YB DB values.yaml + Use ".Values.gflags.tserver.ysql_enable_auth" while setting ysql credentials through YBA +*/}} +{{- define "yugabyte.tserver.readinessProbe" -}} +{{- if .Values.tserver.customReadinessProbe -}} +{{- toYaml .Values.tserver.customReadinessProbe }} +{{- else if .Values.tserver.readinessProbe.enabled -}} +{{- toYaml (omit .Values.tserver.readinessProbe "enabled") }} +exec: + command: + - bash + - -v + - -c + - | + {{- if not .Values.disableYsql }} + {{- if (or .Values.authCredentials.ysql.password (eq .Values.gflags.tserver.ysql_enable_auth "true") .Values.authCredentials.ysql.passwordSecretName) }} + unix_socket=$(find /tmp -name ".yb.*"); + ysqlsh_output=$(ysqlsh -U yugabyte -h "${unix_socket}" -d system_platform -c "\\conninfo"); + exit_code="$?"; + {{- else }} + ysqlsh_output=$(ysqlsh -U yugabyte -h 127.0.0.1 -p {{ index (include "yugabyte.yb_tservers.ports" . | fromYaml) "tcp-ysql-port" }} -d system_platform -c "\\conninfo"); + exit_code="$?"; + {{- end }} + + if [[ $exit_code -ne 0 ]]; then + echo "Error while executing ysqlsh command. Exit code: ${exit_code}"; + echo "Error: ${ysqlsh_output}"; + exit "${exit_code}" + fi + {{- end }} + + {{- if not (eq .Values.gflags.tserver.start_cql_proxy "false") }} + {{- if (and .Values.tls.enabled .Values.tls.clientToServer) }} + ycqlsh_output=$(ycqlsh --debug --ssl -e "SHOW HOST" "$HOSTNAME" {{ index (include "yugabyte.yb_tservers.ports" . | fromYaml) "tcp-yql-port" }} 2>&1); + {{- else }} + ycqlsh_output=$(ycqlsh --debug -e "SHOW HOST" "$HOSTNAME" {{ index (include "yugabyte.yb_tservers.ports" . | fromYaml) "tcp-yql-port" }} 2>&1); + {{- end }} + exit_code="$?"; + + if [[ $exit_code -ne 0 && "${ycqlsh_output}" != *"Remote end requires authentication"* ]]; then + echo "Error while executing ycqlsh command. Exit code: ${exit_code}"; + echo "Error: ${ycqlsh_output}"; + exit "${exit_code}" + fi + {{- end }} + + exit 0 +{{- end -}} +{{- end -}} + +{{/* + Startup Probe for Master +*/}} +{{- define "yugabyte.master.startupProbe" -}} +{{- if .Values.master.customStartupProbe -}} +{{- toYaml .Values.master.customStartupProbe }} +{{- else if .Values.master.startupProbe.enabled -}} +{{- toYaml (omit .Values.master.startupProbe "enabled") }} +tcpSocket: + port: {{ index (include "yugabyte.yb_masters.ports" .| fromYaml) "tcp-rpc-port" }} +{{- end -}} +{{- end -}} + +{{/* + Startup Probe for Tserver +*/}} +{{- define "yugabyte.tserver.startupProbe" -}} +{{- if .Values.tserver.customStartupProbe -}} +{{- toYaml .Values.tserver.customStartupProbe }} +{{- else if .Values.tserver.startupProbe.enabled -}} +{{- toYaml (omit .Values.tserver.startupProbe "enabled") }} +tcpSocket: + port: {{ index (include "yugabyte.yb_tservers.ports" .| fromYaml) "tcp-rpc-port" }} +{{- end -}} +{{- end -}} + +{{/* + Get Security Context. +*/}} +{{- define "getSecurityContext" }} +securityContext: + runAsUser: {{ required "runAsUser cannot be empty" .Values.podSecurityContext.runAsUser }} + runAsGroup: {{ .Values.podSecurityContext.runAsGroup | default 0 }} + fsGroup: {{ .Values.podSecurityContext.fsGroup }} + runAsNonRoot: {{ .Values.podSecurityContext.runAsNonRoot }} +{{- end -}} + +{{/* +Get ipFamily and ipFamilyPolicy settings. +*/}} +{{- define "yugabyte.ipFamilyConfig" }} +{{- if .Values.ipFamilies }} +ipFamilies: + {{- range .Values.ipFamilies }} + - {{ . }} + {{- end }} +{{- end }} +{{- if .Values.ipFamilyPolicy }} +ipFamilyPolicy: {{ .Values.ipFamilyPolicy }} +{{- end }} +{{- end -}} + +{{/* + Append commonNameSuffix to commonName while ensuring total length doesn't exceed 63 characters. +*/}} +{{- define "yugabyte.commonNameWithSuffix" -}} + {{- $commonName := .commonName -}} + {{- $suffix := .root.Values.tls.certManager.certificates.commonNameSuffix | default "" -}} + {{- if $suffix -}} + {{- $combined := printf "%s-%s" $commonName $suffix -}} + {{- if gt (len $combined) 63 -}} + {{- $maxCommonNameLength := sub 63 (add 1 (len $suffix)) -}} + {{- printf "%s-%s" ($commonName | trunc $maxCommonNameLength | trimSuffix "-") $suffix -}} + {{- else -}} + {{- $combined -}} + {{- end -}} + {{- else -}} + {{- $commonName -}} + {{- end -}} +{{- end -}} diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/certificates.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/certificates.yaml new file mode 100644 index 0000000..3dbe868 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/certificates.yaml @@ -0,0 +1,157 @@ +{{- $root := . -}} +--- +{{- if $root.Values.tls.certManager.enabled }} +{{- if $root.Values.tls.certManager.bootstrapSelfsigned }} +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ (printf "%s-bootstrap" (include "yugabyte.tls_cm_issuer" $root)) | quote }} + namespace: "{{ $root.Release.Namespace }}" +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ $root.Values.oldNamingStyle | ternary "yugabyte-ca" (printf "%s-ca" (include "yugabyte.fullname" $root)) }} + namespace: "{{ $root.Release.Namespace }}" +spec: + isCA: true + privateKey: + algorithm: {{ $root.Values.tls.certManager.certificates.algorithm | quote }} + encoding: PKCS8 + size: {{ $root.Values.tls.certManager.certificates.keySize }} + commonName: Yugabyte Selfsigned CA + secretName: {{ $root.Values.oldNamingStyle | ternary "yugabyte-ca" (printf "%s-ca" (include "yugabyte.fullname" $root)) }} + issuerRef: + name: {{ (printf "%s-bootstrap" (include "yugabyte.tls_cm_issuer" $root)) | quote }} + kind: Issuer +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ include "yugabyte.tls_cm_issuer" $root | quote }} + namespace: "{{ $root.Release.Namespace }}" +spec: + ca: + secretName: {{ $root.Values.oldNamingStyle | ternary "yugabyte-ca" (printf "%s-ca" (include "yugabyte.fullname" $root)) }} +--- +{{- else }} +{{/* when bootstrapSelfsigned = false, ie. when using an external CA. +Create a Secret with just the rootCA.cert value and mount into master/tserver pods. +This will be used as a fall back in case the Secret generated by cert-manager does not +have a root ca.crt. This can happen for certain certificate issuers like LetsEncrypt. +*/}} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ printf "%s-root-ca" (include "yugabyte.fullname" $root) }} + namespace: "{{ $root.Release.Namespace }}" + labels: + {{- include "yugabyte.labels" $root | indent 4 }} +type: Opaque +data: + ca.crt: {{ $root.Values.tls.rootCA.cert }} +--- +{{- end }} + + +{{/* +The below Certificate resource will trigger cert-manager to issue crt/key into Secrets. +These secrets are mounted into master/tserver pods. +*/}} +{{- range .Values.Services }} +{{- $service := . -}} +{{- $appLabelArgs := dict "label" .label "root" $root -}} +{{- $serviceValues := (dict "Service" $service "Values" $root.Values "Chart" $root.Chart "Release" $root.Release) -}} +{{- $replicas := (eq .name "yb-masters") | ternary $root.Values.replicas.master $root.Values.replicas.tserver -}} + +{{- if (gt (int $replicas) 0) }} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ $root.Values.oldNamingStyle | ternary (printf "%s-yugabyte-tls-cert" $service.label) (printf "%s-%s-tls-cert" (include "yugabyte.fullname" $root) $service.label) }} + namespace: "{{ $root.Release.Namespace }}" +spec: + secretTemplate: + labels: + {{- include "yugabyte.applabel" ($appLabelArgs) | indent 6 }} + {{- include "yugabyte.labels" $root | indent 6 }} + issuerRef: + name: {{ include "yugabyte.tls_cm_issuer" $root | quote }} + kind: {{ include "yugabyte.tls_issuer_kind" $root }} + group: {{ include "yugabyte.tls_issuer_group" $root }} + secretName: {{ $root.Values.oldNamingStyle | ternary (printf "%s-yugabyte-tls-cert" $service.label) (printf "%s-%s-tls-cert" (include "yugabyte.fullname" $root) $service.label) }} + duration: {{ $root.Values.tls.certManager.certificates.duration | quote }} + renewBefore: {{ $root.Values.tls.certManager.certificates.renewBefore | quote }} + isCA: false + privateKey: + algorithm: {{ $root.Values.tls.certManager.certificates.algorithm | quote }} + encoding: PKCS8 + size: {{ $root.Values.tls.certManager.certificates.keySize }} + rotationPolicy: Always + usages: + - server auth + - client auth + # At least one of a DNS Name, URI, or IP address is required. + dnsNames: + {{- range $index := until ( int ( $replicas ) ) }} + {{- $nodeOldStyle := printf "%s-%d.%s.%s.svc.%s" $service.label $index $service.name $root.Release.Namespace $root.Values.domainName }} + {{- $nodeNewStyle := printf "%s-%s-%d.%s-%s.%s.svc.%s" (include "yugabyte.fullname" $root) $service.label $index (include "yugabyte.fullname" $root) $service.name $root.Release.Namespace $root.Values.domainName }} + + {{- if $root.Values.multicluster.createServiceExports -}} + {{- $nodeOldStyle = printf "%s-%d.%s.%s.%s.svc.clusterset.local" $service.label $index $root.Values.multicluster.kubernetesClusterId $service.name $root.Release.Namespace }} + {{- $nodeNewStyle = printf "%s-%s-%d.%s.%s-%s.%s.svc.clusterset.local" (include "yugabyte.fullname" $root) $service.label $index $root.Values.multicluster.kubernetesClusterId (include "yugabyte.fullname" $root) $service.name $root.Release.Namespace }} + {{- end -}} + + {{- if $root.Values.multicluster.createServicePerPod -}} + {{- $nodeOldStyle = printf "%s-%d.%s.svc.%s" $service.label $index $root.Release.Namespace $root.Values.domainName }} + {{- $nodeNewStyle = printf "%s-%s-%d.%s.svc.%s" (include "yugabyte.fullname" $root) $service.label $index $root.Release.Namespace $root.Values.domainName }} + {{- end -}} + {{- $node := $root.Values.oldNamingStyle | ternary $nodeOldStyle $nodeNewStyle }} + - {{$node}} + {{- end }} + - {{ printf "%s-%s.%s.svc.%s" (include "yugabyte.fullname" $root) $service.name $root.Release.Namespace $root.Values.domainName }} + uris: [] + ipAddresses: [] + {{- if $root.Values.tls.certManager.certificates.commonNameRequired }} + commonName: {{ include "yugabyte.commonNameWithSuffix" (dict "commonName" ($root.Values.oldNamingStyle | ternary $service.name (printf "%s-%s" (include "yugabyte.fullname" $root) $service.name)) "root" $root) | quote }} + {{- end }} +--- +{{- end }} +{{- end }} + +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ $root.Values.oldNamingStyle | ternary "yugabyte-tls-client-cert" (printf "%s-client-tls" (include "yugabyte.fullname" $root)) }} + namespace: "{{ $root.Release.Namespace }}" +spec: + secretTemplate: + labels: + {{- include "yugabyte.labels" $root | indent 6 }} + issuerRef: + name: {{ include "yugabyte.tls_cm_issuer" $root | quote }} + kind: {{ include "yugabyte.tls_issuer_kind" $root }} + group: {{ include "yugabyte.tls_issuer_group" $root }} + secretName: {{ $root.Values.oldNamingStyle | ternary "yugabyte-tls-client-cert" (printf "%s-client-tls" (include "yugabyte.fullname" $root)) }} + duration: {{ $root.Values.tls.certManager.certificates.duration | quote }} + renewBefore: {{ $root.Values.tls.certManager.certificates.renewBefore | quote }} + commonName: {{ include "yugabyte.commonNameWithSuffix" (dict "commonName" "yugabyte" "root" $root) | quote }} + isCA: false + privateKey: + algorithm: {{ $root.Values.tls.certManager.certificates.algorithm | quote }} + encoding: PKCS8 + size: {{ $root.Values.tls.certManager.certificates.keySize }} + rotationPolicy: Always + usages: + - client auth + dnsNames: [] + uris: [] + ipAddresses: [] +--- +{{- end }} diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/controller-gflags-secret.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/controller-gflags-secret.yaml new file mode 100644 index 0000000..a30ad16 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/controller-gflags-secret.yaml @@ -0,0 +1,36 @@ +{{- $root := . -}} +{{- range $service := $root.Values.Services }} +{{- if and (eq $service.name "yb-tservers") (and ($root.Values.ybc.enabled) ($root.Values.ybc.useYBDBImage)) }} +{{- $serviceValues := (dict "Service" $service "Values" $root.Values "Chart" $root.Chart "Release" $root.Release) -}} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "yugabyte.fullname" $root }}-controller-gflags + namespace: "{{ $root.Release.Namespace }}" +type: Opaque +stringData: + server.conf.template: | + --v=1 + --server_address={{ (eq $root.Values.ip_version_support "v6_only") | ternary "[::]" "0.0.0.0" }} + --server_port={{ index $service.ports "tcp-ybc-port" }} + --log_dir=/mnt/disk0/ybc-data/controller/logs + --yb_tserver_address={{ include "yugabyte.rpc_bind_address" $serviceValues }} + --yb_tserver_webserver_port={{ index $service.ports "http-ui" }} + --yb_admin=/home/yugabyte/tserver/bin/yb-admin + --ysql_dump=/home/yugabyte/tserver/postgres/bin/ysql_dump + --ysql_dumpall=/home/yugabyte/tserver/postgres/bin/ysql_dumpall + --ysqlsh=/home/yugabyte/tserver/postgres/bin/ysqlsh + --ycqlsh=/home/yugabyte/tserver/bin/ycqlsh + --log_filename=yb-controller-server + --log_utc_time=true +{{- if and $root.Values.tls.enabled $root.Values.tls.nodeToNode }} + --certs_dir_name=/opt/certs/yugabyte + --cert_node_filename={{ $root.Values.tserver.serverBroadcastAddress | default (include "yugabyte.server_fqdn" $serviceValues) }} +{{- end }} +{{- range $flag, $override := $root.Values.gflags.ybc }} + --{{ $flag }}={{ $override }} +{{- end }} +--- +{{- end }} +{{- end }} diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/debug_config_map.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/debug_config_map.yaml new file mode 100644 index 0000000..a15c4fc --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/debug_config_map.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "yugabyte.fullname" . }}-master-hooks + namespace: "{{ .Release.Namespace }}" +data: +{{- range $index := until ( int ( .Values.replicas.master ) ) }} + yb-master-{{.}}-pre_debug_hook.sh: "echo 'hello-from-pre' " + yb-master-{{.}}-post_debug_hook.sh: "echo 'hello-from-post' " +{{- end }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "yugabyte.fullname" . }}-tserver-hooks + namespace: "{{ .Release.Namespace }}" +data: +{{- range $index := until ( int ( .Values.replicas.tserver) ) }} + yb-tserver-{{.}}-pre_debug_hook.sh: "echo 'hello-from-pre' " + yb-tserver-{{.}}-post_debug_hook.sh: "echo 'hello-from-post' " +{{- end }} +--- diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/hooks/setup-credentials-job.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/hooks/setup-credentials-job.yaml new file mode 100644 index 0000000..c63f92c --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/hooks/setup-credentials-job.yaml @@ -0,0 +1,104 @@ +{{- if or .Values.authCredentials.ycql.user .Values.authCredentials.ycql.password .Values.authCredentials.ycql.keyspace .Values.authCredentials.ysql.password .Values.authCredentials.ysql.user .Values.authCredentials.ysql.database .Values.authCredentials.ycql.passwordSecretName .Values.authCredentials.ysql.passwordSecretName .Values.authCredentials.ycql.passwordSecretName .Values.authCredentials.ysql.passwordSecretName .Values.authCredentials.ycql.passwordSecretName .Values.authCredentials.ysql.passwordSecretName}} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "yugabyte.fullname" . }}-setup-credentials + namespace: "{{ .Release.Namespace }}" + labels: + app: "setup-credentials" + release: {{ .Release.Name | quote }} + chart: "{{ .Chart.Name }}" + component: "{{ .Values.Component }}" + annotations: + "helm.sh/hook": post-install + "helm.sh/hook-weight": "0" + "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation +spec: + backoffLimit: 2 + template: + metadata: + name: "setup-credentials" + labels: + app: "setup-credentials" + release: {{ .Release.Name | quote }} + chart: "{{ .Chart.Name }}" + component: "{{ .Values.Component }}" + spec: + {{- if .Values.Image.pullSecretName }} + imagePullSecrets: + - name: {{ .Values.Image.pullSecretName }} + {{ end }} + {{- if .Values.nodeSelector }} + nodeSelector: {{ toYaml .Values.nodeSelector | nindent 8 }} + {{- end }} + {{- if .Values.tserver.tolerations }} + tolerations: {{ toYaml .Values.tserver.tolerations | nindent 8 }} + {{- end }} + restartPolicy: Never + containers: + - name: setup-credentials + image: "{{ .Values.Image.repository }}:{{ .Values.Image.tag }}" + env: + {{- if .Values.authCredentials.ysql.user }} + - name: YSQL_USER + value: "{{ .Values.authCredentials.ysql.user }}" + {{- end }} + {{- if or .Values.authCredentials.ysql.password .Values.authCredentials.ysql.passwordSecretName }} + - name: YSQL_PASSWORD + {{- if .Values.authCredentials.ysql.passwordSecretName }} + valueFrom: + secretKeyRef: + name: {{ .Values.authCredentials.ysql.passwordSecretName }} + key: ysqlPassword + {{- else }} + value: "{{ .Values.authCredentials.ysql.password }}" + {{- end -}} + {{- end }} + {{- if .Values.authCredentials.ysql.database }} + - name: YSQL_DB + value: "{{ .Values.authCredentials.ysql.database }}" + {{- end }} + {{- if .Values.authCredentials.ycql.user }} + - name: YCQL_USER + value: "{{ .Values.authCredentials.ycql.user }}" + {{- end }} + {{- if or (.Values.authCredentials.ycql.password) (.Values.authCredentials.ycql.passwordSecretName) }} + - name: YCQL_PASSWORD + {{- if .Values.authCredentials.ycql.passwordSecretName }} + valueFrom: + secretKeyRef: + name: {{ .Values.authCredentials.ycql.passwordSecretName }} + key: ycqlPassword + {{- else }} + value: "{{ .Values.authCredentials.ycql.password }}" + {{- end -}} + {{- end }} + {{- if .Values.authCredentials.ycql.keyspace }} + - name: YCQL_KEYSPACE + value: "{{ .Values.authCredentials.ycql.keyspace }}" + {{- end }} + {{- if .Values.tls.enabled }} + - name: SSL_CERTFILE + value: "/root/.yugabytedb/root.crt" + {{- end }} + command: + - 'bash' + - '/home/yugabyte/bin/setup-credentials/setup-credentials.sh' + volumeMounts: + - name: setup-credentials-script + mountPath: "/home/yugabyte/bin/setup-credentials" + {{- if .Values.tls.enabled }} + - name: yugabyte-tls-client-cert + mountPath: "/root/.yugabytedb/" + {{- end }} + volumes: + - name: setup-credentials-script + configMap: + name: {{ include "yugabyte.fullname" . }}-setup-credentials-script + {{- if .Values.tls.enabled }} + - name: yugabyte-tls-client-cert + secret: + secretName: {{ .Values.oldNamingStyle | ternary "yugabyte-tls-client-cert" (printf "%s-client-tls" (include "yugabyte.fullname" . )) }} + defaultMode: 256 + {{- end }} +{{- end }} diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/master-gflags-secret.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/master-gflags-secret.yaml new file mode 100644 index 0000000..0a86c21 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/master-gflags-secret.yaml @@ -0,0 +1,58 @@ +{{- $root := . -}} +{{- range $service := $root.Values.Services }} +{{- if eq $service.name "yb-masters" }} +{{- $serviceValues := (dict "Service" $service "Values" $root.Values "Chart" $root.Chart "Release" $root.Release) -}} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "yugabyte.fullname" $root }}-master-gflags + namespace: "{{ $root.Release.Namespace }}" +type: Opaque +stringData: + server.conf.template: | +{{- if not $root.Values.storage.ephemeral }} + --fs_data_dirs={{ template "yugabyte.fs_data_dirs" $root.Values.storage.master }} +{{- else }} + --fs_data_dirs=/var/yugabyte +{{- end }} +{{- if eq $root.Values.ip_version_support "v6_only" }} + --net_address_filter=ipv6_external,ipv6_non_link_local,ipv6_all,ipv4_external,ipv4_all +{{- end }} +{{- if $root.Values.isMultiAz }} + --master_addresses={{ $root.Values.masterAddresses }} + --replication_factor={{ $root.Values.replicas.totalMasters }} +{{- else }} + --master_addresses={{ template "yugabyte.master_addresses" $root }} + --replication_factor={{ $root.Values.replicas.master }} +{{- end }} +{{- if not $root.Values.disableYsql }} + --enable_ysql=true +{{- else }} + --enable_ysql=false +{{- end }} +{{- if $root.Values.tls.enabled }} + --certs_dir=/opt/certs/yugabyte + --use_node_to_node_encryption={{ $root.Values.tls.nodeToNode }} + --allow_insecure_connections={{ $root.Values.tls.insecure }} +{{- end }} +{{- if $root.Values.yugabytedUi.enabled }} + --master_enable_metrics_snapshotter={{ $root.Values.yugabytedUi.metricsSnapshotter.enabled }} + --metrics_snapshotter_tserver_metrics_whitelist={{ join "," $root.Values.yugabytedUi.metricsSnapshotter.whitelist }} +{{- end }} + --metric_node_name=${EXPORTED_INSTANCE} + --memory_limit_hard_bytes={{ template "yugabyte.memory_hard_limit" dict "size" $root.Values.resource.master.limits.memory "limitPercent" $root.Values.master.memoryLimitHardPercentage }} + --stderrthreshold=0 + --num_cpus={{ ceil $root.Values.resource.master.requests.cpu }} + --max_log_size=256 + --undefok=num_cpus,enable_ysql + --use_node_hostname_for_local_tserver=true + --rpc_bind_addresses={{ include "yugabyte.rpc_bind_address" $serviceValues }} + --server_broadcast_addresses={{ include "yugabyte.server_broadcast_address" $serviceValues }} + --webserver_interface={{ include "yugabyte.webserver_interface" $serviceValues }} +{{- range $flag, $override := $root.Values.gflags.master }} + --{{ $flag }}={{ $override }} +{{- end }} +{{- end }} +{{- end }} +--- diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/master-servicemonitor.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/master-servicemonitor.yaml new file mode 100644 index 0000000..0e3c431 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/master-servicemonitor.yaml @@ -0,0 +1,51 @@ +{{- if and .Values.serviceMonitor.enabled .Values.serviceMonitor.master.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "yugabyte.fullname" . }}-yb-master + labels: + {{- if .Values.oldNamingStyle }} + app: "yb-master" + {{- else }} + app.kubernetes.io/name: "yb-master" + {{- end }} + release: {{ .Release.Name | quote }} + chart: "{{ .Chart.Name }}" + component: "{{ .Values.Component }}" + {{- with .Values.serviceMonitor.extraLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + jobLabel: "release" + selector: + matchLabels: + {{- if .Values.oldNamingStyle }} + app: "yb-master" + {{- else }} + app.kubernetes.io/name: "yb-master" + {{- end }} + release: {{ .Release.Name | quote }} + service-type: "headless" + endpoints: + + {{- with .Values.serviceMonitor.master }} + {{- if .enabled }} + - port: {{ .port }} + path: {{ .path }} + {{- if .interval }} + interval: {{ .interval }} + {{- else }} + interval: {{ $.Values.serviceMonitor.interval }} + {{- end }} + relabelings: + - targetLabel: "group" + replacement: "yb-master" + - targetLabel: "export_type" + replacement: "master_export" + - targetLabel: "node_prefix" + replacement: {{ $.Release.Name | quote }} + metricRelabelings: + {{- toYaml $.Values.serviceMonitor.commonMetricRelabelings | nindent 4 }} + {{- end }} + {{- end }} +{{- end }} diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/multicluster/common-tserver-service.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/multicluster/common-tserver-service.yaml new file mode 100644 index 0000000..68f250d --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/multicluster/common-tserver-service.yaml @@ -0,0 +1,23 @@ +{{- if (and .Values.multicluster.createCommonTserverService (not .Values.oldNamingStyle)) }} +{{- range $service := .Values.serviceEndpoints }} +{{- if eq $service.name "yb-tserver-service" }} +{{- $appLabelArgs := dict "label" $service.app "root" $ -}} +apiVersion: v1 +kind: Service +metadata: + name: "yb-tserver-common" + labels: + {{- include "yugabyte.applabel" ($appLabelArgs) | indent 4 }} + {{- include "yugabyte.labels" $ | indent 4 }} +spec: + ports: + {{- range $label, $port := $service.ports }} + - name: {{ $label | quote }} + port: {{ $port }} + {{- end }} + selector: + {{- include "yugabyte.appselector" ($appLabelArgs) | indent 4 }} +--- +{{- end }} +{{- end }} +{{- end }} diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/multicluster/mcs-service-export.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/multicluster/mcs-service-export.yaml new file mode 100644 index 0000000..eeafcb1 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/multicluster/mcs-service-export.yaml @@ -0,0 +1,21 @@ +{{- /* + Ref - https://cloud.google.com/kubernetes-engine/docs/how-to/multi-cluster-services#registering_a_service_for_export + https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#exporting-services +*/}} +{{- if .Values.multicluster.createServiceExports }} +apiVersion: {{ .Values.multicluster.mcsApiVersion }} +kind: ServiceExport +metadata: + name: {{ .Values.oldNamingStyle | ternary "yb-masters" (printf "%s-%s" (include "yugabyte.fullname" .) "yb-masters") | quote }} + namespace: "{{ .Release.Namespace }}" + labels: + {{- include "yugabyte.labels" . | indent 4 }} +--- +apiVersion: {{ .Values.multicluster.mcsApiVersion }} +kind: ServiceExport +metadata: + name: {{ .Values.oldNamingStyle | ternary "yb-tservers" (printf "%s-%s" (include "yugabyte.fullname" .) "yb-tservers") | quote }} + namespace: "{{ .Release.Namespace }}" + labels: + {{- include "yugabyte.labels" . | indent 4 }} +{{ end -}} diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/multicluster/service-per-pod.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/multicluster/service-per-pod.yaml new file mode 100644 index 0000000..334f438 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/multicluster/service-per-pod.yaml @@ -0,0 +1,35 @@ +{{- if .Values.multicluster.createServicePerPod }} +{{- range $server := .Values.Services }} +{{- range $replicaNum := until (int (ternary $.Values.replicas.master $.Values.replicas.tserver (eq $server.name "yb-masters"))) }} +{{- $appLabelArgs := dict "label" $server.label "root" $ -}} +{{- $podName := $.Values.oldNamingStyle | ternary $server.label (printf "%s-%s" (include "yugabyte.fullname" $) $server.label) -}} +{{- $podName := printf "%s-%d" $podName $replicaNum -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ $podName | quote }} + labels: + {{- include "yugabyte.applabel" ($appLabelArgs) | indent 4 }} + {{- include "yugabyte.labels" $ | indent 4 }} + service-type: "non-endpoint" +spec: + ports: + {{- range $label, $port := $server.ports }} + {{- if or (eq $label "grpc-ybc-port") (eq $label "tcp-ybc-port")}} + {{- if $.Values.ybc.enabled }} + - name: "tcp-ybc-port" + port: {{ $port }} + {{- end }} + {{- else }} + - name: {{ $label | quote }} + port: {{ $port }} + {{- end }} + {{- end}} + selector: + statefulset.kubernetes.io/pod-name: {{ $podName | quote }} + {{- include "yugabyte.appselector" ($appLabelArgs) | indent 4 }} + {{- include "yugabyte.ipFamilyConfig" $ | indent 2 }} +--- +{{- end }} +{{- end }} +{{- end }} diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/otel-collector.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/otel-collector.yaml new file mode 100644 index 0000000..8167ccb --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/otel-collector.yaml @@ -0,0 +1,154 @@ +{{- $root := . -}} +--- +{{- if .Values.otelCollector.enabled }} +--- +{{- if .Values.otelCollector.secretEnv }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "yugabyte.fullname" $root }}-otel-secrets + namespace: "{{ $root.Release.Namespace }}" +type: Opaque +data: +{{- range .Values.otelCollector.secretEnv }} + {{ .envName }}: {{ .envValue | quote }} +{{- end }} +{{- end }} +--- +apiVersion: opentelemetry.io/v1beta1 +kind: OpenTelemetryCollector +metadata: + name: {{ $root.Values.oldNamingStyle | ternary "otel-yb-tserver" (printf "otel-%s-yb-tserver" (include "yugabyte.fullname" $root)) | quote }} + namespace: "{{ $root.Release.Namespace }}" +spec: + config: + exporters: + {{- range $name, $exporter := .Values.otelCollector.exporters}} + {{ $name }}: +{{ toYaml $exporter | indent 8 }} + {{- end }} + extensions: + file_storage/queue: + create_directory: true + compaction: + directory: /mnt/disk0/otel-collector/queue + on_rebound: true + on_start: true + rebound_needed_threshold_mib: 100 + rebound_trigger_threshold_mib: 10 + directory: /mnt/disk0/otel-collector/queue + health_check: + endpoint: {{ (printf "%s:13133" (include "yugabyte.webserver_interface" $root)) | quote }} + path: "/health" + processors: + {{- range $name := (keys .Values.otelCollector.exporters) }} + attributes/{{ $name }}: + actions: + - action: upsert + key: ddsource + value: yugabyte + - action: upsert + key: service + value: yb-otel-collector + - action: upsert + key: host + value: ${POD_NAME} + {{- end }} + receivers: + filelog/ysql: + attributes: + audit_log_type: ysql + exclude: + - /mnt/disk0/yb-data/tserver/logs/*.gz + include: + - /mnt/disk0/yb-data/tserver/logs/postgresql-*.log + multiline: + line_start_pattern: {{ $root.Values.otelCollector.recievers.ysql.lineStartPattern | quote }} + operators: + - expr: 'body not matches "^.*\\w+: AUDIT:(.|\\n|\\r|\\s)*$"' + type: filter + - on_error: drop + regex: {{ $root.Values.otelCollector.recievers.ysql.regex | quote }} + timestamp: +{{ toYaml $root.Values.otelCollector.recievers.ysql.timestamp | indent 12 }} + type: regex_parser + start_at: beginning + storage: file_storage/queue + service: + extensions: + - file_storage/queue + - health_check + pipelines: + {{- range $name := (keys .Values.otelCollector.exporters) }} + logs/{{ $name }}: + exporters: + - {{ $name }} + processors: + - attributes/{{ $name }} + receivers: + - filelog/ysql + {{- end}} + telemetry: + logs: + output_paths: + - /mnt/disk0/otel-collector/logs/otel-collector.logs + image: {{ .Values.otelCollector.image | default "otel/opentelemetry-collector-contrib:0.114.0" | quote }} + ipFamilyPolicy: SingleStack + managementState: managed + mode: sidecar + replicas: 1 + resources: {{ .Values.otelCollector.resources | toYaml | nindent 4 }} + upgradeStrategy: automatic + env: + - name: USER + value: yugabyte + {{- range .Values.otelCollector.secretEnv }} + {{- if eq .envName "GOOGLE_APPLICATION_CREDENTIALS_CONTENT" }} + - name: GOOGLE_APPLICATION_CREDENTIALS + value: /mnt/disk0/otel-collector/gcp_credentials/gcp_credentials.json + {{- else }} + - name: {{ .envName }} + valueFrom: + secretKeyRef: + name: {{ include "yugabyte.fullname" $root }}-otel-secrets + key: {{ .envName }} + {{- end }} + {{- end }} + volumeMounts: + - mountPath: /mnt/disk0/ + name: {{ $root.Values.oldNamingStyle | ternary "datadir0" (printf "%s0" (include "yugabyte.volume_name" $root)) }} + initContainers: + - name: init-otel-collector + image: "{{ $root.Values.Image.repository }}:{{ $root.Values.Image.tag }}" + imagePullPolicy: {{ $root.Values.Image.pullPolicy }} + command: + - "sh" + - "-c" + - > + mkdir -p /mnt/disk0/otel-collector/logs; + rm -rf /mnt/disk0/otel-collector/gcp_credentials; + if [ -n "${GOOGLE_APPLICATION_CREDENTIALS_CONTENT}" ] ; then + mkdir -p /mnt/disk0/otel-collector/gcp_credentials; + echo ${GOOGLE_APPLICATION_CREDENTIALS_CONTENT} > /mnt/disk0/otel-collector/gcp_credentials/gcp_credentials.json; + fi + volumeMounts: + - mountPath: /mnt/disk0/ + name: {{ $root.Values.oldNamingStyle | ternary "datadir0" (printf "%s0" (include "yugabyte.volume_name" $root)) }} + env: + {{- range .Values.otelCollector.secretEnv }} + {{- if eq .envName "GOOGLE_APPLICATION_CREDENTIALS_CONTENT" }} + - name: {{ .envName }} + valueFrom: + secretKeyRef: + name: {{ include "yugabyte.fullname" $root }}-otel-secrets + key: {{ .envName }} + {{- end }} + {{- end }} + {{- if .Values.podSecurityContext.enabled }} + {{- include "getSecurityContext" . | nindent 2 }} + {{- else if not .Values.ocpCompatibility.enabled }} + securityContext: + runAsUser: 0 + {{- end }} +--- +{{- end }} diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/secrets.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/secrets.yaml new file mode 100644 index 0000000..0bd9034 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/secrets.yaml @@ -0,0 +1,7 @@ +{{- $root := . -}} +--- # Create secrets from other namespaces for masters. +{{- $data := dict "secretenv" $.Values.master.secretEnv "root" . "suffix" "master"}} +{{- include "yugabyte.envsecrets" $data }} +--- # Create secrets from other namespaces for tservers. +{{- $data := dict "secretenv" $.Values.tserver.secretEnv "root" . "suffix" "tserver" }} +{{- include "yugabyte.envsecrets" $data }} \ No newline at end of file diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/service-endpoints.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/service-endpoints.yaml new file mode 100644 index 0000000..d3cdd23 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/service-endpoints.yaml @@ -0,0 +1,93 @@ +{{- $root := . -}} +--- +# Services endpoints +{{- if $root.Values.enableLoadBalancer }} +{{- range .Values.Services }} +{{- $service := . -}} +{{- $appLabelArgs := dict "label" .label "root" $root -}} +{{- range $endpoint := $root.Values.serviceEndpoints }} +{{- $serviceScopeArgs := dict "endpoint" $endpoint "defaultScope" $root.Values.defaultServiceScope }} +{{- $scope := include "yugabyte.servicescope" $serviceScopeArgs }} +{{- if eq $service.label $endpoint.app }} +# Only render if new naming style or old naming style + AZ scope +{{- if or (not $root.Values.oldNamingStyle) (eq $scope "AZ") }} +{{- $servicenameargs := dict "root" $root "endpoint" $endpoint "scope" $scope }} +{{- $serviceName := include "yugabyte.servicename" ($servicenameargs) }} +{{- $serviceArgs := dict "endpoint" $endpoint "serviceName" $serviceName "root" $root "scope" $scope }} +{{- $service := include "yugabyte.should_render_service" ($serviceArgs) }} +# Render if: +# 1. Always if scope is AZ +# 2. Namespaced scope: Service does not exist +# 3. Namespaced scope: Service exists and is owned by this release +{{- if eq $service "true" }} +{{- if (or (ne $endpoint.name "yugabyted-ui-service") $root.Values.yugabytedUi.enabled) }} +apiVersion: v1 +kind: Service +metadata: + name: {{ $serviceName | quote }} + namespace: "{{ $root.Release.Namespace }}" + annotations: + # Keep resource for namespaced services + {{- if eq $scope "Namespaced" }} + "helm.sh/resource-policy": keep + {{- end }} + {{- if $endpoint.annotations }} +{{ toYaml $endpoint.annotations | nindent 4 }} + {{- end }} + labels: + # scope is "Namespaced" or "AZ" + scope: {{ $scope }} + serviceName: {{ $endpoint.name }} + service-type: "endpoint" + {{- include "yugabyte.applabel" ($appLabelArgs) | indent 4 }} + {{- $labels := include "yugabyte.labels" $root | fromYaml }} + # For Namespaced service, remove zone and release name filter + {{- if eq $scope "Namespaced" }} + {{- $labels = omit $labels "yugabyte.io/zone" "release" }} + {{- end }} + {{- range $key,$value := $labels }} + {{ $key }}: {{ $value | quote }} + {{- end }} +spec: + {{- if eq $root.Release.Service "Tiller" }} + clusterIP: + {{- else }} + {{- if $endpoint.clusterIP }} + clusterIP: {{ $endpoint.clusterIP }} + {{- end }} + {{- end }} + type: {{ $endpoint.type }} + ports: + {{- range $label, $port := $endpoint.ports }} + - name: {{ $label | quote }} + port: {{ $port }} + {{- end }} + selector: + {{- if eq $endpoint.name "yugabyted-ui-service"}} + yugabytedUi: "true" + {{- else if eq $scope "Namespaced" }} + {{- include "yugabyte.namespacedserviceselector" ($appLabelArgs) | indent 4 }} + {{- else }} + {{- include "yugabyte.appselector" ($appLabelArgs) | indent 4 }} + {{- end }} + {{- if ne $endpoint.type "ClusterIP" }} + externalTrafficPolicy: {{ $endpoint.externalTrafficPolicy | default "Cluster" }} + {{- end }} + {{- if $endpoint.loadBalancerIP }} + loadBalancerIP: {{ $endpoint.loadBalancerIP }} + {{- end }} + {{- if eq $endpoint.name "yugabyted-ui-service"}} + sessionAffinity: {{ $endpoint.sessionAffinity }} + {{- end }} + {{- include "yugabyte.ipFamilyConfig" ($root) | indent 2 }} + {{- range $key, $value := $endpoint.extraArgs }} + {{ $key }}: {{ $value | quote }} + {{- end }} +--- +{{- end }} +{{- end }} +{{- end }} +{{- end }} +{{- end }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/service.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/service.yaml new file mode 100644 index 0000000..1e7adc3 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/service.yaml @@ -0,0 +1,874 @@ +{{- $root := . -}} +--- +{{- if and (eq $root.Values.tls.enabled true) (eq $root.Values.tls.certManager.enabled false) }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ $root.Values.oldNamingStyle | ternary "yugabyte-tls-client-cert" (printf "%s-client-tls" (include "yugabyte.fullname" $root)) }} + namespace: "{{ $root.Release.Namespace }}" + labels: + {{- include "yugabyte.labels" $root | indent 4 }} +type: Opaque +data: +{{- if $root.Values.tls.rootCA.key }} +{{- $rootCAClient := buildCustomCert $root.Values.tls.rootCA.cert $root.Values.tls.rootCA.key -}} +{{- $client := genSignedCert "yugabyte" ( default nil ) ( default nil ) 3650 $rootCAClient }} + root.crt: {{ $rootCAClient.Cert | b64enc }} + yugabytedb.crt: {{ $client.Cert | b64enc }} + yugabytedb.key: {{ $client.Key | b64enc }} +{{- else }} + root.crt: {{ $root.Values.tls.rootCA.cert }} + yugabytedb.crt: {{ $root.Values.tls.clientCert.cert }} + yugabytedb.key: {{ $root.Values.tls.clientCert.key }} +{{- end }} +--- +{{- end }} +--- +{{- range .Values.Services }} +{{- $service := . -}} +{{- $appLabelArgs := dict "label" .label "root" $root -}} +{{- $serviceValues := (dict "Service" $service "Values" $root.Values "Chart" $root.Chart "Release" $root.Release) -}} +{{- $replicas := (eq .name "yb-masters") | ternary $root.Values.replicas.master $root.Values.replicas.tserver -}} + +{{- if gt (int ($replicas)) 0 }} +{{- if and (eq $root.Values.tls.enabled true) (eq $root.Values.tls.certManager.enabled false) }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ $root.Values.oldNamingStyle | ternary (printf "%s-yugabyte-tls-cert" $service.label) (printf "%s-%s-tls-cert" (include "yugabyte.fullname" $root) $service.label) }} + namespace: "{{ $root.Release.Namespace }}" + labels: + {{- include "yugabyte.applabel" ($appLabelArgs) | indent 4 }} + {{- include "yugabyte.labels" $root | indent 4 }} +type: Opaque +data: +{{- range $index := until ( int ( $replicas ) ) }} +{{- $nodeOldStyle := printf "%s-%d.%s.%s.svc.%s" $service.label $index $service.name $root.Release.Namespace $root.Values.domainName }} +{{- $nodeNewStyle := printf "%s-%s-%d.%s-%s.%s.svc.%s" (include "yugabyte.fullname" $root) $service.label $index (include "yugabyte.fullname" $root) $service.name $root.Release.Namespace $root.Values.domainName }} + +{{- if $root.Values.multicluster.createServiceExports -}} + {{- $nodeOldStyle = printf "%s-%d.%s.%s.%s.svc.clusterset.local" $service.label $index $root.Values.multicluster.kubernetesClusterId $service.name $root.Release.Namespace }} + {{- $nodeNewStyle = printf "%s-%s-%d.%s.%s-%s.%s.svc.clusterset.local" (include "yugabyte.fullname" $root) $service.label $index $root.Values.multicluster.kubernetesClusterId (include "yugabyte.fullname" $root) $service.name $root.Release.Namespace }} +{{- end -}} + +{{- if $root.Values.multicluster.createServicePerPod -}} + {{- $nodeOldStyle = printf "%s-%d.%s.svc.%s" $service.label $index $root.Release.Namespace $root.Values.domainName }} + {{- $nodeNewStyle = printf "%s-%s-%d.%s.svc.%s" (include "yugabyte.fullname" $root) $service.label $index $root.Release.Namespace $root.Values.domainName }} +{{- end -}} + +{{- $node := $root.Values.oldNamingStyle | ternary $nodeOldStyle $nodeNewStyle }} +{{- if $root.Values.tls.rootCA.key }} +{{- $dns1 := printf "*.%s-%s.%s" (include "yugabyte.fullname" $root) $service.name $root.Release.Namespace }} +{{- $dns2 := printf "%s.svc.%s" $dns1 $root.Values.domainName }} +{{- if $root.Values.multicluster.createServiceExports -}} + {{- $dns1 = printf "*.%s.%s-%s.%s.svc.clusterset.local" $root.Values.multicluster.kubernetesClusterId (include "yugabyte.fullname" $root) $service.name $root.Release.Namespace }} +{{- end -}} +{{- if $root.Values.multicluster.createServicePerPod -}} + {{- $dns1 = printf "*.%s.svc.%s" $root.Release.Namespace $root.Values.domainName }} +{{- end -}} +{{- $rootCA := buildCustomCert $root.Values.tls.rootCA.cert $root.Values.tls.rootCA.key -}} +{{- $server := genSignedCert $node ( default nil ) (list $node $dns1 $dns2 ) 3650 $rootCA }} + node.{{$node}}.crt: {{ $server.Cert | b64enc }} + node.{{$node}}.key: {{ $server.Key | b64enc }} +{{- else }} + node.{{$node}}.crt: {{ $root.Values.tls.nodeCert.cert }} + node.{{$node}}.key: {{ $root.Values.tls.nodeCert.key }} +{{- end }} +{{- end }} + ca.crt: {{ $root.Values.tls.rootCA.cert }} +{{- end }} + +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ $root.Values.oldNamingStyle | ternary .name (printf "%s-%s" (include "yugabyte.fullname" $root) .name) | quote }} + labels: + {{- include "yugabyte.applabel" ($appLabelArgs) | indent 4 }} + {{- include "yugabyte.labels" $root | indent 4 }} + service-type: "headless" +spec: + clusterIP: None + publishNotReadyAddresses: true + ports: + {{- range $label, $port := .ports }} + {{- if or (eq $label "grpc-ybc-port") (eq $label "tcp-ybc-port")}} + {{- if $root.Values.ybc.enabled }} + - name: "tcp-ybc-port" + port: {{ $port }} + {{- end }} + {{- else if (eq $label "yugabyted-ui") }} + {{- if $root.Values.yugabytedUi.enabled }} + - name: {{ $label | quote }} + port: {{ $port }} + {{- end }} + {{- else }} + - name: {{ $label | quote }} + port: {{ $port }} + {{- end }} + {{- end }} + selector: + {{- include "yugabyte.appselector" ($appLabelArgs) | indent 4 }} + {{- include "yugabyte.ipFamilyConfig" ($root) | indent 2 }} + +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ $root.Values.oldNamingStyle | ternary .label (printf "%s-%s" (include "yugabyte.fullname" $root) .label) | quote }} + namespace: "{{ $root.Release.Namespace }}" + labels: + {{- include "yugabyte.applabel" ($appLabelArgs) | indent 4 }} + {{- include "yugabyte.labels" $root | indent 4 }} + {{- if $root.Values.statefulSetAnnotations }} + annotations: +{{ toYaml $root.Values.statefulSetAnnotations | indent 4 }} + {{- end }} +spec: + serviceName: {{ $root.Values.oldNamingStyle | ternary .name (printf "%s-%s" (include "yugabyte.fullname" $root) .name) | quote }} + podManagementPolicy: {{ $root.Values.PodManagementPolicy }} + {{ if eq .name "yb-masters" }} + replicas: {{ $root.Values.replicas.master }} + {{ else }} + replicas: {{ $root.Values.replicas.tserver }} + {{ end }} + {{- $storageInfo := (eq .name "yb-masters") | ternary $root.Values.storage.master $root.Values.storage.tserver -}} + {{ if not $root.Values.storage.ephemeral }} + volumeClaimTemplates: + {{- range $index := until (int ($storageInfo.count )) }} + - metadata: + name: {{ $root.Values.oldNamingStyle | ternary (printf "datadir%d" $index) (printf "%s%d" (include "yugabyte.volume_name" $root) $index) }} + {{- if $root.Values.legacyVolumeClaimAnnotations }} + annotations: + volume.beta.kubernetes.io/storage-class: {{ $storageInfo.storageClass | quote }} + {{- end }} + labels: + {{- include "yugabyte.labels" $root | indent 10 }} + spec: + accessModes: + - "ReadWriteOnce" + {{- if $storageInfo.storageClass }} + storageClassName: {{ $storageInfo.storageClass }} + {{- end }} + resources: + requests: + storage: {{ $storageInfo.size }} + {{- end }} + {{- end }} + updateStrategy: + {{- if eq $root.Values.updateStrategy.type "RollingUpdate" }} + type: RollingUpdate + rollingUpdate: + {{- if eq .name "yb-masters" }} + partition: {{ $root.Values.partition.master }} + {{- else }} + partition: {{ $root.Values.partition.tserver }} + {{- end }} + {{- else }} + type: {{ $root.Values.updateStrategy.type }} + {{- end }} + selector: + matchLabels: + {{- include "yugabyte.appselector" ($appLabelArgs) | indent 6 }} + template: + metadata: + annotations: + {{- if eq .name "yb-masters" }} + checksum/gflags: {{ (not $root.Values.master.gflagsChecksum) | ternary (include (print $root.Template.BasePath "/master-gflags-secret.yaml") $root | sha256sum) $root.Values.master.gflagsChecksum }} + {{- if (or $root.Values.networkAnnotation $root.Values.master.podAnnotations $root.Values.tls.enabled) }} + {{- with $root.Values.networkAnnotation }}{{ toYaml . | nindent 8 }}{{ end }} + {{- with $root.Values.master.podAnnotations }}{{ toYaml . | nindent 8 }}{{ end }} + {{- if $root.Values.tls.enabled }} + checksum/rootCA: {{ (not $root.Values.tls.rootCAChecksum) | ternary (cat $root.Values.tls.rootCA.cert $root.Values.tls.rootCA.key | sha256sum) $root.Values.tls.rootCAChecksum }} + {{- end }} + {{- end }} + {{- else }} + checksum/gflags: {{ (not $root.Values.tserver.gflagsChecksum) | ternary (include (print $root.Template.BasePath "/tserver-gflags-secret.yaml") $root | sha256sum) $root.Values.tserver.gflagsChecksum }} + {{- if $root.Values.ybc.useYBDBImage }} + checksum/ybcGflags: {{ (not $root.Values.ybc.gflagsChecksum) | ternary (include (print $root.Template.BasePath "/controller-gflags-secret.yaml") $root | sha256sum) $root.Values.ybc.gflagsChecksum }} + {{- end }} + {{- if (or $root.Values.networkAnnotation $root.Values.tserver.podAnnotations $root.Values.tls.enabled) }} + {{- with $root.Values.networkAnnotation }}{{ toYaml . | nindent 8 }}{{ end }} + {{- with $root.Values.tserver.podAnnotations }}{{ toYaml . | nindent 8 }}{{ end }} + {{- if $root.Values.tls.enabled }} + checksum/rootCA: {{ (not $root.Values.tls.rootCAChecksum) | ternary (cat $root.Values.tls.rootCA.cert $root.Values.tls.rootCA.key | sha256sum) $root.Values.tls.rootCAChecksum }} + {{- end }} + {{- end }} + {{- if and (eq .name "yb-tservers") ($root.Values.otelCollector.enabled) }} + sidecar.opentelemetry.io/inject: {{ $root.Values.oldNamingStyle | ternary "otel-yb-tserver" (printf "otel-%s-%s" (include "yugabyte.fullname" $root) .label) | quote }} + checksum/otel: {{ $root.Values.otelCollector | toYaml | sha256sum }} # Required to trigger a pod restart when the config changes + {{- end}} + {{- end }} + labels: + {{- include "yugabyte.applabel" ($appLabelArgs) | indent 8 }} + {{- include "yugabyte.labels" $root | indent 8 }} + {{- if $root.Values.istioCompatibility.enabled }} + sidecar.istio.io/inject: "true" + {{- end }} + {{- if eq .name "yb-masters" }} + {{- with $root.Values.master.podLabels }}{{ toYaml . | nindent 8 }}{{ end }} + {{- else }} + {{- with $root.Values.tserver.podLabels }}{{ toYaml . | nindent 8 }}{{ end }} + {{- end }} + {{- if $root.Values.yugabytedUi.enabled }} + yugabytedUi: "true" + {{- end }} + spec: + {{- if $root.Values.Image.pullSecretName }} + imagePullSecrets: + - name: {{ $root.Values.Image.pullSecretName }} + {{ end }} + {{- if $root.Values.podSecurityContext.enabled }} + securityContext: {{- omit $root.Values.podSecurityContext "enabled" | toYaml | nindent 8 }} + {{- end }} + {{- if $root.Values.nodeSelector }} + nodeSelector: + {{- with $root.Values.nodeSelector }}{{ toYaml . | nindent 8 }}{{ end }} + {{- end }} + {{- if eq .name "yb-masters" }} # yb-masters + {{- with $root.Values.master.serviceAccount }} + serviceAccountName: {{ . }} + {{- end }} + {{- if $root.Values.master.tolerations }} + tolerations: + {{- with $root.Values.master.tolerations }}{{ toYaml . | nindent 8 }}{{ end }} + {{- end }} + {{- else }} # yb-tservers + {{- with $root.Values.tserver.serviceAccount }} + serviceAccountName: {{ . }} + {{- end }} + {{- if $root.Values.tserver.tolerations }} + tolerations: + {{- with $root.Values.tserver.tolerations }}{{ toYaml . | nindent 8 }}{{ end }} + {{- end }} + {{- end }} + terminationGracePeriodSeconds: 300 + affinity: + # Set the anti-affinity selector scope to YB masters and tservers. + {{- $nodeAffinityData := dict}} + {{- if eq .name "yb-masters" -}} + {{- $nodeAffinityData = get $root.Values.master.affinity "nodeAffinity" | default (dict) -}} + {{- else -}} + {{- $nodeAffinityData = get $root.Values.tserver.affinity "nodeAffinity" | default (dict) -}} + {{- end -}} + {{ if $root.Values.AZ }} + {{- $userSelectorTerms := dig "requiredDuringSchedulingIgnoredDuringExecution" "nodeSelectorTerms" "" $nodeAffinityData | default (list) -}} + {{- $baseAffinity := include "yugabyte.multiAZNodeAffinity" $root | fromYaml -}} + {{- $requiredSchedule := (list) -}} + {{- if $userSelectorTerms -}} + {{- range $userSelectorTerms -}} + {{- $userTerm := . -}} + {{- range $baseAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms -}} + {{- $matchExpr := concat .matchExpressions $userTerm.matchExpressions | dict "matchExpressions" -}} + {{- $requiredSchedule = mustMerge $matchExpr $userTerm | append $requiredSchedule -}} + {{- end -}} + {{- end -}} + {{- else -}} + {{- $requiredSchedule = $baseAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms -}} + {{- end -}} + + {{- with $baseAffinity.requiredDuringSchedulingIgnoredDuringExecution -}} + {{- $_ := set . "nodeSelectorTerms" $requiredSchedule -}} + {{- end -}} + {{- $nodeAffinityData = mustMerge $baseAffinity $nodeAffinityData -}} + {{- end -}} + + {{- $podAntiAffinityData := dict -}} + {{- $basePodAntiAffinity := include "yugabyte.podAntiAffinity" ($appLabelArgs) | fromYaml -}} + {{- if eq .name "yb-masters" -}} + {{- with $root.Values.master.affinity -}} + {{- $userPodAntiAffinity := get . "podAntiAffinity" | default (dict) -}} + {{- if $userPodAntiAffinity -}} + {{- $preferredList := dig "preferredDuringSchedulingIgnoredDuringExecution" "" $userPodAntiAffinity | default (list) | concat $basePodAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution}} + {{- $_ := set $basePodAntiAffinity "preferredDuringSchedulingIgnoredDuringExecution" $preferredList -}} + {{- end -}} + {{- $podAntiAffinityData = mustMerge $basePodAntiAffinity $userPodAntiAffinity -}} + {{- end -}} + {{- else -}} + {{- with $root.Values.tserver.affinity -}} + {{- $userPodAntiAffinity := get . "podAntiAffinity" | default (dict) -}} + {{- if $userPodAntiAffinity -}} + {{- $preferredList := dig "preferredDuringSchedulingIgnoredDuringExecution" "" $userPodAntiAffinity | default (list) | concat $basePodAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution}} + {{- $_ := set $basePodAntiAffinity "preferredDuringSchedulingIgnoredDuringExecution" $preferredList -}} + {{- end -}} + {{- $podAntiAffinityData = mustMerge $basePodAntiAffinity $userPodAntiAffinity -}} + {{- end -}} + {{- end -}} + + {{- if eq .name "yb-masters" -}} + {{- if $nodeAffinityData -}} + {{- $_ := set $root.Values.master.affinity "nodeAffinity" $nodeAffinityData -}} + {{- end -}} + {{- $_ := set $root.Values.master.affinity "podAntiAffinity" $podAntiAffinityData -}} + {{ toYaml $root.Values.master.affinity | nindent 8 }} + {{- else -}} + {{- if $nodeAffinityData -}} + {{- $_ := set $root.Values.tserver.affinity "nodeAffinity" $nodeAffinityData -}} + {{- end -}} + {{- $_ := set $root.Values.tserver.affinity "podAntiAffinity" $podAntiAffinityData -}} + {{ toYaml $root.Values.tserver.affinity | nindent 8 }} + {{ end }} + {{- with $root.Values.dnsConfig }} + dnsConfig: {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $root.Values.dnsPolicy }} + dnsPolicy: {{ . | quote }} + {{- end }} + {{- if and $root.Values.tls.enabled $root.Values.tls.certManager.enabled }} + initContainers: + - name: certmanager-init + image: {{ $root.Values.Image.repository }}:{{ $root.Values.Image.tag }} + imagePullPolicy: {{ $root.Values.Image.pullPolicy }} + command: + - "/bin/bash" + - "-c" + - | + {{ $serverFqdn := include "yugabyte.server_fqdn" $serviceValues }} + mkdir -p /mnt/disk0/certs; + chmod u+x /mnt/disk0/certs; + if [ -L /mnt/disk0/certs/certs ]; then + echo "Cleaning up old recursive symlink at /mnt/disk0/certs/certs if it exists"; + chmod --reference="$(dirname /mnt/disk0/certs)" /mnt/disk0/certs + rm -f /mnt/disk0/certs/certs; + fi + echo "Creating ephemeral /opt/certs/yugabyte/ as symlink to persisted /mnt/disk0/certs/" + ln -sF /mnt/disk0/certs /opt/certs/yugabyte + if [[ ! -f /mnt/disk0/certs/ca.crt ]]; then + echo "Fresh install of /mnt/disk0/certs/ca.crt" + cp /home/yugabyte/cert-manager/ca.crt /mnt/disk0/certs/ca.crt; + fi + cmp -s /home/yugabyte/cert-manager/ca.crt /mnt/disk0/certs/ca.crt; sameRootCA=$?; + if [[ $sameRootCA -eq 0 ]]; then + echo "Refreshing TLS certs at /mnt/disk0/certs/"; + cp /home/yugabyte/cert-manager/tls.crt /mnt/disk0/certs/node.{{$serverFqdn}}.crt; + cp /home/yugabyte/cert-manager/tls.key /mnt/disk0/certs/node.{{$serverFqdn}}.key; + chmod 600 /mnt/disk0/certs/*; + else + echo "WARNING: Not refreshing certificates as the root ca.crt has changed"; + fi + volumeMounts: + {{- if not $root.Values.storage.ephemeral }} + - name: {{ $root.Values.oldNamingStyle | ternary (printf "datadir0") (printf "%s0" (include "yugabyte.volume_name" $root)) }} + mountPath: /mnt/disk0 + {{- end }} + - name: {{ $root.Values.oldNamingStyle | ternary (printf "%s-yugabyte-tls-cert" .label) (printf "%s-%s-tls-cert" (include "yugabyte.fullname" $root) .label) }} + mountPath: /home/yugabyte/cert-manager + readOnly: true + - name: certs-dir + mountPath: /opt/certs + env: + - name: HOSTNAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + {{- end }} + containers: + - name: "{{ .label }}" + image: "{{ $root.Values.Image.repository }}:{{ $root.Values.Image.tag }}" + imagePullPolicy: {{ $root.Values.Image.pullPolicy }} + lifecycle: + postStart: + exec: + command: + - "bash" + - "-c" + - > + mkdir -p /mnt/disk0/cores; + mkdir -p /mnt/disk0/yb-data/scripts; + if [ ! -f /mnt/disk0/yb-data/scripts/log_cleanup.sh ]; then + if [ -f /home/yugabyte/bin/log_cleanup.sh ]; then + cp /home/yugabyte/bin/log_cleanup.sh /mnt/disk0/yb-data/scripts; + fi; + fi + {{- if (and (not $root.Values.storage.ephemeral) (not $service.skipHealthChecks)) }} + {{- if $root.Values.livenessProbe.enabled }} + {{- if $root.Values.customLivenessProbe }} + livenessProbe: {{- toYaml $root.Values.customLivenessProbe | nindent 10 }} + {{- else }} + livenessProbe: + exec: + command: + - bash + - -v + - -c + - | + {{- include "yugabyte.fs_data_dirs_probe" $storageInfo | nindent 14 }}; + exit_code="$?"; + echo "disk check exited with: ${exit_code}"; + exit "${exit_code}" + failureThreshold: {{ $root.Values.livenessProbe.failureThreshold }} + periodSeconds: {{ $root.Values.livenessProbe.periodSeconds }} + successThreshold: {{ $root.Values.livenessProbe.successThreshold }} + timeoutSeconds: {{ $root.Values.livenessProbe.timeoutSeconds }} + {{- end }} + {{- end }} + {{- if (and (eq .name "yb-masters") (include "yugabyte.master.startupProbe" $root)) }} + startupProbe: {{- include "yugabyte.master.startupProbe" $root | nindent 10 }} + {{- else if (and (eq .name "yb-tservers") (include "yugabyte.tserver.startupProbe" $root)) }} + startupProbe: {{- include "yugabyte.tserver.startupProbe" $root | nindent 10 }} + {{- end }} + {{- end }} + {{- if (and (eq .name "yb-masters") (include "yugabyte.master.readinessProbe" $root)) }} + readinessProbe: {{- include "yugabyte.master.readinessProbe" $root | nindent 10 }} + {{- else if (and (eq .name "yb-tservers") (include "yugabyte.tserver.readinessProbe" $root)) }} + readinessProbe: {{- include "yugabyte.tserver.readinessProbe" $root | nindent 10 }} + {{- end }} + env: + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: HOSTNAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: YBDEVOPS_CORECOPY_DIR + value: "/mnt/disk0/cores" + {{- if eq .name "yb-masters" }} + {{- with $root.Values.master.extraEnv }}{{ toYaml . | nindent 8 }}{{- end }} + {{- $data := dict "secretenv" $root.Values.master.secretEnv "root" $root "suffix" "master"}} + {{- include "yugabyte.addenvsecrets" $data | nindent 8 }} + {{- else }} + {{- with $root.Values.tserver.extraEnv }}{{ toYaml . | nindent 8 }}{{- end }} + {{- $data := dict "secretenv" $root.Values.tserver.secretEnv "root" $root "suffix" "tserver" }} + {{- include "yugabyte.addenvsecrets" $data | nindent 8 }} + {{- end }} + {{- if and $root.Values.tls.enabled $root.Values.tls.clientToServer (ne .name "yb-masters") }} + - name: SSL_CERTFILE + value: /root/.yugabytedb/root.crt + {{- end }} + resources: + {{- if eq .name "yb-masters" }} +{{ toYaml $root.Values.resource.master | indent 10 }} + {{ else }} +{{ toYaml $root.Values.resource.tserver | indent 10 }} + {{ end }} + # core dumps are collected to workingDir if + # kernel.core_pattern is set to a relative path like + # core.%e.%p.%t ref: + # https://github.com/yugabyte/charts/issues/11 + workingDir: "/mnt/disk0/cores" + command: + - "/sbin/tini" + - "--" + args: + - "/bin/bash" + - "-c" + - | + {{- $preCommands := ( (eq .name "yb-masters") | ternary $root.Values.master.advanced.preCommands $root.Values.tserver.advanced.preCommands ) }} {{- with $preCommands }} {{ . | nindent 12 }} {{- end }} + {{- if and (not $root.Values.preflight.skipUlimit) (not $root.Values.preflight.skipAll) }} + if [ -f /home/yugabyte/tools/k8s_preflight.py ]; then + /home/yugabyte/tools/k8s_preflight.py all + fi && \ + {{- end }} + {{- if (and (not $root.Values.storage.ephemeral) (not $root.Values.preflight.skipAll)) }} + {{- include "yugabyte.fs_data_dirs_probe" $storageInfo | nindent 12 }} && \ + {{- end }} + {{- $rpcAddr := include "yugabyte.rpc_bind_address" $serviceValues -}} + {{- $rpcPort := index $service.ports "tcp-rpc-port" -}} + {{- $rpcDict := dict "Addr" $rpcAddr "Port" $rpcPort -}} + {{- $rpcPreflight := include "yugabyte.preflight_check" (set $serviceValues "Preflight" $rpcDict) -}} + {{- if $rpcPreflight -}}{{ $rpcPreflight | nindent 12 }}{{ end -}} + {{- $broadcastAddr := include "yugabyte.server_broadcast_address" $serviceValues -}} + {{/* skip bind check for servicePerPod multi-cluster, we cannot/don't bind to service IP */}} + {{- if not $root.Values.multicluster.createServicePerPod }} + {{- $broadcastPort := index $service.ports "tcp-rpc-port" -}} + {{- $broadcastDict := dict "Addr" $broadcastAddr "Port" $broadcastPort -}} + {{- $broadcastPreflight := include "yugabyte.preflight_check" (set $serviceValues "Preflight" $broadcastDict) -}} + {{- if $broadcastPreflight -}}{{ $broadcastPreflight | nindent 12 }}{{ end -}} + {{- end }} + {{- $webserverAddr := include "yugabyte.webserver_interface" $serviceValues -}} + {{- $webserverPort := index $service.ports "http-ui" -}} + {{- $webserverDict := dict "Addr" $webserverAddr "Port" $webserverPort -}} + {{- $webserverPreflight := include "yugabyte.preflight_check" (set $serviceValues "Preflight" $webserverDict) -}} + {{- if $webserverPreflight -}}{{ $webserverPreflight | nindent 12 }}{{ end }} + if [[ -f /home/yugabyte/tools/k8s_parent.py ]]; then + k8s_parent="/home/yugabyte/tools/k8s_parent.py" + else + k8s_parent="" + fi && \ + {{- if eq .name "yb-masters" }} + mkdir -p /tmp/yugabyte/master/conf && \ + envsubst < /opt/master/conf/server.conf.template > /tmp/yugabyte/master/conf/server.conf && \ + exec ${k8s_parent} /home/yugabyte/bin/yb-master \ + --flagfile /tmp/yugabyte/master/conf/server.conf + {{- else }} + {{- $cqlAddr := include "yugabyte.cql_proxy_bind_address" $serviceValues -}} + {{- $cqlPort := index $service.ports "tcp-yql-port" -}} + {{- $cqlDict := dict "Addr" $cqlAddr "Port" $cqlPort -}} + {{- $cqlPreflight := include "yugabyte.preflight_check" (set $serviceValues "Preflight" $cqlDict) -}} + {{- if $cqlPreflight -}}{{ $cqlPreflight | nindent 12 }}{{ end -}} + {{- $ysqlAddr := include "yugabyte.pgsql_proxy_bind_address" $serviceValues -}} + {{- $ysqlPort := index $service.ports "tcp-ysql-port" -}} + {{- if not $root.Values.disableYsql -}} + {{- $ysqlDict := dict "Addr" $ysqlAddr "Port" $ysqlPort -}} + {{- $ysqlPreflight := include "yugabyte.preflight_check" (set $serviceValues "Preflight" $ysqlDict) -}} + {{- if $ysqlPreflight -}}{{ $ysqlPreflight | nindent 12 }}{{ end -}} + {{- end }} + mkdir -p /tmp/yugabyte/tserver/conf && \ + envsubst < /opt/tserver/conf/server.conf.template > /tmp/yugabyte/tserver/conf/server.conf && \ + exec ${k8s_parent} /home/yugabyte/bin/yb-tserver \ + --flagfile /tmp/yugabyte/tserver/conf/server.conf + {{- end }} + ports: + {{- range $label, $port := .ports }} + {{- if (eq $label "yugabyted-ui") }} + {{- if $root.Values.yugabytedUi.enabled }} + - containerPort: {{ $port }} + name: {{ $label | quote }} + {{- end }} + {{- else if not (or (eq $label "grpc-ybc-port") (eq $label "tcp-ybc-port"))}} + - containerPort: {{ $port }} + name: {{ $label | quote }} + {{- end }} + {{- end}} + volumeMounts: + {{- if (eq .name "yb-tservers") }} + - name: tserver-tmp + mountPath: /tmp + - name: tserver-gflags + mountPath: /opt/tserver/conf + {{- else if (eq .name "yb-masters") }} + - name: master-gflags + mountPath: /opt/master/conf + {{- end }} + - name: debug-hooks-volume + mountPath: /opt/debug_hooks_config + {{ if not $root.Values.storage.ephemeral }} + {{- range $index := until (int ($storageInfo.count)) }} + - name: {{ $root.Values.oldNamingStyle | ternary (printf "datadir%d" $index) (printf "%s%d" (include "yugabyte.volume_name" $root) $index) }} + mountPath: /mnt/disk{{ $index }} + {{- end }} + {{- end }} + {{- if $root.Values.tls.enabled }} + - name: {{ $root.Values.oldNamingStyle | ternary (printf "%s-yugabyte-tls-cert" .label) (printf "%s-%s-tls-cert" (include "yugabyte.fullname" $root) .label) }} + mountPath: {{ $root.Values.tls.certManager.enabled | ternary "/home/yugabyte/cert-manager" "/opt/certs/yugabyte" }} + readOnly: true + {{- if $root.Values.tls.certManager.enabled }} + - name: certs-dir + mountPath: /opt/certs + {{- end }} + - name: {{ $root.Values.oldNamingStyle | ternary "yugabyte-tls-client-cert" (printf "%s-client-tls" (include "yugabyte.fullname" $root)) }} + mountPath: /root/.yugabytedb/ + readOnly: true + {{- end }} + {{- if and (eq .name "yb-masters") ($root.Values.master.extraVolumeMounts) -}} + {{- include "yugabyte.isExtraVolumesMappingExists" $root.Values.master -}} + {{- $root.Values.master.extraVolumeMounts | toYaml | nindent 10 -}} + {{- else if and (eq .name "yb-tservers") ($root.Values.tserver.extraVolumeMounts) -}} + {{- include "yugabyte.isExtraVolumesMappingExists" $root.Values.tserver -}} + {{- $root.Values.tserver.extraVolumeMounts | toYaml | nindent 10 -}} + {{- end -}} + + {{ if not $root.Values.storage.ephemeral }} + - name: yb-cleanup + image: "{{ $root.Values.Image.repository }}:{{ $root.Values.Image.tag }}" + imagePullPolicy: {{ $root.Values.Image.pullPolicy }} + env: + - name: USER + value: "yugabyte" + {{- if $root.Values.otelCollector.enabled }} + - name: PRESERVE_AUDIT_LOGS # Allows the exporter sufficient time to collect logs before they are deleted. + value: "true" + {{- end }} + command: + - "/sbin/tini" + - "--" + args: + - "/bin/bash" + - "-c" + - > + while true; do + sleep 3600; + /home/yugabyte/scripts/log_cleanup.sh; + done + volumeMounts: + - name: {{ $root.Values.oldNamingStyle | ternary "datadir0" (printf "%s0" (include "yugabyte.volume_name" $root)) }} + mountPath: /home/yugabyte/ + subPath: yb-data + - name: {{ $root.Values.oldNamingStyle | ternary "datadir0" (printf "%s0" (include "yugabyte.volume_name" $root)) }} + mountPath: /var/yugabyte/cores + subPath: cores + {{- if $root.Values.ybCleanup.resources }} + resources: {{ toYaml $root.Values.ybCleanup.resources | nindent 10 }} + {{- end }} + {{- end }} + + {{- if $root.Values.yugabytedUi.enabled }} + - name: yugabyted-ui + image: "{{ $root.Values.Image.repository }}:{{ $root.Values.Image.tag }}" + imagePullPolicy: "IfNotPresent" + env: + - name: HOSTNAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: YUGABYTED_UI_K8S + value: "true" + command: + - "/sbin/tini" + - "--" + args: + - "/bin/bash" + - "-c" + - | + {{- if $root.Values.yugabytedUi.recoverWithoutFailure }} + while true; do + {{- end }} + /home/yugabyte/bin/yugabyted-ui \ + {{- $rpcAddr := include "yugabyte.rpc_bind_address" $serviceValues }} + -database_host={{ $rpcAddr }} \ + {{- $webserverAddr := include "yugabyte.webserver_interface" $serviceValues }} + -bind_address={{ $webserverAddr }} \ + {{- $masterPort := "7000" }} + {{- $tserverPort := "9000" }} + {{- $ysqlPort := "5433" }} + {{- $ycqlPort := "9042" }} + {{- range $root.Values.Services -}} + {{- if eq .name "yb-masters" -}} + {{- $masterPort = index .ports "http-ui" -}} + {{- else if eq .name "yb-tservers" -}} + {{- $tserverPort = index .ports "http-ui" -}} + {{- $ysqlPort = index .ports "tcp-ysql-port" -}} + {{- $ycqlPort = index .ports "tcp-yql-port" -}} + {{- end -}} + {{- end }} + -ysql_port={{ $ysqlPort }} \ + -ycql_port={{ $ycqlPort }} \ + -master_ui_port={{ $masterPort }} \ + -tserver_ui_port={{ $tserverPort }} \ + {{- if $root.Values.tls.enabled }} + -secure={{ $root.Values.tls.enabled }} \ + {{- end }} + {{- if $root.Values.authCredentials.ysql.user }} + -ysql_username={{ $root.Values.authCredentials.ysql.user }} \ + {{- end }} + {{- if $root.Values.authCredentials.ycql.user }} + -ycql_username={{ $root.Values.authCredentials.ycql.user }} \ + {{- end }} + {{- if $root.Values.authCredentials.ysql.password }} + -ysql_password={{ $root.Values.authCredentials.ysql.password }} \ + {{- end }} + {{- if $root.Values.authCredentials.ycql.password }} + -ycql_password={{ $root.Values.authCredentials.ycql.password }} \ + {{- end }} + || echo "ERROR: yugabyted-ui failed. This might be because your yugabyte \ + version is older than 2.21.0. If this is the case, set yugabytedUi.enabled to false \ + in helm to disable yugabyted-ui, or upgrade to a version 2.21.0 or newer."; \ + {{- if $root.Values.yugabytedUi.recoverWithoutFailure }} + echo "Attempting restart in {{ $root.Values.yugabytedUi.recoveryInterval }}." + trap break TERM INT; \ + sleep {{ $root.Values.yugabytedUi.recoveryInterval }} & wait; \ + trap - TERM INT; + done \ + {{- end }} + {{- end }} + + {{- if and (eq .name "yb-tservers") ($root.Values.ybc.enabled) }} + - name: yb-controller + image: "{{ $root.Values.Image.repository }}:{{ $root.Values.Image.tag }}" + imagePullPolicy: {{ $root.Values.Image.pullPolicy }} + {{- if $root.Values.ybc.useYBDBImage }} + env: + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: HOSTNAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: YBDEVOPS_CORECOPY_DIR + value: "/mnt/disk0/cores" + workingDir: "/mnt/disk0/cores" + command: + - "/sbin/tini" + - "--" + args: + - "/bin/bash" + - "-c" + - | + if [[ -f /home/yugabyte/tools/k8s_parent.py ]]; then + k8s_parent="/home/yugabyte/tools/k8s_parent.py" + else + k8s_parent="" + fi && \ + mkdir -p /tmp/yugabyte/controller/conf && \ + envsubst < /opt/controller/conf/server.conf.template > /tmp/yugabyte/controller/conf/server.conf && \ + exec ${k8s_parent} /home/yugabyte/controller/bin/yb-controller-server \ + --flagfile /tmp/yugabyte/controller/conf/server.conf + {{- else }} + lifecycle: + postStart: + exec: + command: + - "bash" + - "-c" + - > + mkdir -p /mnt/disk0/yw-data/controller/tmp; + mkdir -p /mnt/disk0/yw-data/controller/conf; + mkdir -p /mnt/disk0/ybc-data/controller/logs; + mkdir -p /tmp/yugabyte/controller; + ln -sf /mnt/disk0/ybc-data/controller/logs /tmp/yugabyte/controller; + ln -sf /mnt/disk0/yw-data/controller/bin /tmp/yugabyte/controller; + rm -f /tmp/yugabyte/controller/yb-controller.pid; + command: + - "/sbin/tini" + - "--" + args: + - "/bin/bash" + - "-c" + - > + while true; do + sleep 60; + /home/yugabyte/tools/k8s_ybc_parent.py status || /home/yugabyte/tools/k8s_ybc_parent.py start; + done + {{- end }} + {{- range $label, $port := .ports }} + {{- if or (eq $label "grpc-ybc-port") (eq $label "tcp-ybc-port")}} + ports: + - containerPort: {{ $port }} + name: "tcp-ybc-port" + {{- end }} + {{- end }} + volumeMounts: + - name: tserver-tmp + mountPath: /tmp + {{- if $root.Values.ybc.useYBDBImage }} + - name: controller-gflags + mountPath: /opt/controller/conf + {{- end }} + {{- if not $root.Values.storage.ephemeral }} + {{- range $index := until (int ($storageInfo.count)) }} + - name: {{ $root.Values.oldNamingStyle | ternary (printf "datadir%d" $index) (printf "%s%d" (include "yugabyte.volume_name" $root) $index) }} + mountPath: /mnt/disk{{ $index }} + {{- end }} + {{- end }} + {{- if $root.Values.tls.enabled }} + - name: {{ $root.Values.oldNamingStyle | ternary (printf "%s-yugabyte-tls-cert" .label) (printf "%s-%s-tls-cert" (include "yugabyte.fullname" $root) .label) }} + mountPath: {{ $root.Values.tls.certManager.enabled | ternary "/home/yugabyte/cert-manager" "/opt/certs/yugabyte" }} + readOnly: true + {{- if $root.Values.tls.certManager.enabled }} + - name: certs-dir + mountPath: /opt/certs + {{- end }} + {{- end }} + {{- if ($root.Values.tserver.extraVolumeMounts) -}} + {{- include "yugabyte.isExtraVolumesMappingExists" $root.Values.tserver -}} + {{- $root.Values.tserver.extraVolumeMounts | toYaml | nindent 10 -}} + {{- end -}} + {{- if $root.Values.ybc.resources }} + resources: {{ toYaml $root.Values.ybc.resources | nindent 10 }} + {{- end }} + {{- end}} + + volumes: + {{- if (eq .name "yb-masters") }} + - name: debug-hooks-volume + configMap: + name: {{ include "yugabyte.fullname" $root }}-master-hooks + defaultMode: 0755 + - name: master-gflags + secret: + secretName: {{ include "yugabyte.fullname" $root }}-master-gflags + defaultMode: 0755 + {{- else if (eq .name "yb-tservers") }} + - name: debug-hooks-volume + configMap: + name: {{ include "yugabyte.fullname" $root }}-tserver-hooks + defaultMode: 0755 + - name: tserver-gflags + secret: + secretName: {{ include "yugabyte.fullname" $root }}-tserver-gflags + defaultMode: 0755 + {{- if $root.Values.ybc.useYBDBImage }} + - name: controller-gflags + secret: + secretName: {{ include "yugabyte.fullname" $root }}-controller-gflags + defaultMode: 0755 + {{- end }} + - name: tserver-tmp + emptyDir: {} + {{- end }} + {{- if $root.Values.tls.enabled }} + {{- if $root.Values.tls.certManager.enabled }} + {{- /* certManager enabled */}} + - name: {{ $root.Values.oldNamingStyle | ternary (printf "%s-yugabyte-tls-cert" .label) (printf "%s-%s-tls-cert" (include "yugabyte.fullname" $root) .label) }} + projected: + sources: + {{- if not $root.Values.tls.certManager.bootstrapSelfsigned }} + - secret: + name: {{ printf "%s-root-ca" (include "yugabyte.fullname" $root) }} + {{- end }} + - secret: + name: {{ $root.Values.oldNamingStyle | ternary (printf "%s-yugabyte-tls-cert" .label) (printf "%s-%s-tls-cert" (include "yugabyte.fullname" $root) .label) }} + - name: certs-dir + emptyDir: {} + {{- else }} + {{/* certManager disabled */}} + - name: {{ $root.Values.oldNamingStyle | ternary (printf "%s-yugabyte-tls-cert" .label) (printf "%s-%s-tls-cert" (include "yugabyte.fullname" $root) .label) }} + secret: + secretName: {{ $root.Values.oldNamingStyle | ternary (printf "%s-yugabyte-tls-cert" .label) (printf "%s-%s-tls-cert" (include "yugabyte.fullname" $root) .label) }} + defaultMode: 256 + {{- end }} + - name: {{ $root.Values.oldNamingStyle | ternary "yugabyte-tls-client-cert" (printf "%s-client-tls" (include "yugabyte.fullname" $root)) }} + secret: + secretName: {{ $root.Values.oldNamingStyle | ternary "yugabyte-tls-client-cert" (printf "%s-client-tls" (include "yugabyte.fullname" $root)) }} + {{- if $root.Values.tls.certManager.enabled }} + items: + - key: ca.crt + path: root.crt + - key: tls.crt + path: yugabytedb.crt + - key: tls.key + path: yugabytedb.key + {{- end }} + defaultMode: 256 + {{- end }} + {{- if and (eq .name "yb-masters") ($root.Values.master.extraVolumes) -}} + {{- include "yugabyte.isExtraVolumesMappingExists" $root.Values.master -}} + {{- $root.Values.master.extraVolumes | toYaml | nindent 8 -}} + {{- else if and (eq .name "yb-tservers") ($root.Values.tserver.extraVolumes) -}} + {{- include "yugabyte.isExtraVolumesMappingExists" $root.Values.tserver -}} + {{- $root.Values.tserver.extraVolumes | toYaml | nindent 8 -}} + {{- end -}} +{{- if and $root.Values.useOldPodDisruptionBudget (not $root.Values.isMultiAz) }} +--- +{{/* +TODO: switch to policy/v1 completely when we stop supporting +Kubernetes versions < 1.21 +*/}} +{{- if $root.Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" }} +apiVersion: policy/v1 +{{- else }} +apiVersion: policy/v1 +{{- end }} +kind: PodDisruptionBudget +metadata: + name: {{ $root.Values.oldNamingStyle | ternary (printf "%s-pdb" .label) (printf "%s-%s-pdb" (include "yugabyte.fullname" $root) .name) }} +spec: + maxUnavailable: {{ template "yugabyte.max_unavailable_for_quorum" $root }} + selector: + matchLabels: + {{- include "yugabyte.appselector" ($appLabelArgs) | indent 6 }} +{{- end }} +{{- end }} +{{- end }} diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/setup-credentials-configmap.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/setup-credentials-configmap.yaml new file mode 100644 index 0000000..8d6d457 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/setup-credentials-configmap.yaml @@ -0,0 +1,262 @@ +{{- if or .Values.authCredentials.ycql.user .Values.authCredentials.ycql.password .Values.authCredentials.ycql.keyspace .Values.authCredentials.ysql.password .Values.authCredentials.ysql.user .Values.authCredentials.ysql.database .Values.authCredentials.ysql.passwordSecretName .Values.authCredentials.ycql.passwordSecretName}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "yugabyte.fullname" . }}-setup-credentials-script + namespace: "{{ .Release.Namespace }}" + labels: + release: {{ .Release.Name | quote }} + chart: "{{ .Chart.Name }}" + component: "{{ .Values.Component }}" +data: + setup-credentials.sh: | + #!/bin/bash + + set -eo pipefail + + # Setup script to setup credentials + + # ----------------------------------------- + # Default Variables + # ----------------------------------------- + + readonly DEFAULT_YSQL_USER="yugabyte" + readonly DEFAULT_YSQL_PASSWORD="yugabyte" + readonly DEFAULT_YSQL_DB="yugabyte" + + readonly DEFAULT_YCQL_USER="cassandra" + readonly DEFAULT_YCQL_PASSWORD="cassandra" + + {{- range .Values.Services }} + {{- $service := . -}} + {{- if eq ($service.name) "yb-tservers" }} + readonly YSQL_PORT={{ index $service.ports "tcp-ysql-port" }} + # TODO: Update the tcp-yql-port to tcp-ycql-port in values.yaml + readonly YCQL_PORT={{ index $service.ports "tcp-yql-port" }} + {{- end }} + {{- end }} + + {{- $serviceName := .Values.oldNamingStyle | ternary "yb-tservers" (printf "%s-yb-tservers" (include "yugabyte.fullname" . )) }} + {{- $tserverFQDN := printf "%s.%s.svc.%s" $serviceName .Release.Namespace .Values.domainName }} + + prefix_ysql_cmd=( + /home/yugabyte/bin/ysqlsh -h {{ $tserverFQDN }} + -p "$YSQL_PORT" + ) + + prefix_ycql_cmd=( + /home/yugabyte/bin/ycqlsh {{ $tserverFQDN }} + "$YCQL_PORT" + ) + + {{- if .Values.tls.enabled }} + prefix_ysql_cmd+=("sslmode=require") + prefix_ycql_cmd+=(--ssl) + {{- end }} + + # ----------------------------------------- + # Variables + # ----------------------------------------- + + ysql_user= + ysql_password= + ysql_db= + ycql_user= + ycql_password= + ycql_keyspace= + + # ----------------------------------------- + # Hepler functions + # ----------------------------------------- + + cleanup() { + local exit_code=$? + echo "Exiting with code $exit_code" + exit "$exit_code" + } + + function waitUntilHealthy() { + declare -a ysql_cmd + export PGPASSWORD="$2" + ysql_cmd=( + /home/yugabyte/bin/ysqlsh -h {{ $tserverFQDN }} + -p "$3" + -U "$1" + -c "\\conninfo" + ) + + if [[ "$4" == "true" ]]; then + ysql_cmd+=("sslmode=require") + fi + + echo "${ysql_cmd[@]}" + while ! "${ysql_cmd[@]}"; do + sleep 5s + done + } + + export -f waitUntilHealthy + + get_ysql_credentials() { + [[ -n "$YSQL_USER" ]] && ysql_user="$YSQL_USER" || ysql_user="$DEFAULT_YSQL_USER" + + [[ -n "$YSQL_PASSWORD" ]] && ysql_password="$YSQL_PASSWORD" + + if [[ -z "$YSQL_PASSWORD" ]] && [[ "$ysql_user" != "$DEFAULT_YSQL_USER" ]]; then + ysql_password="$YSQL_USER" + fi + + [[ -n "$YSQL_DB" ]] && ysql_db="$YSQL_DB" + + [[ -z "$YSQL_DB" ]] && [[ -n "$YSQL_USER" ]] && ysql_db="$ysql_user" + + api="ysql" + } + + get_ycql_credentials() { + [[ -n "$YCQL_USER" ]] && ycql_user="$YCQL_USER" || ycql_user="$DEFAULT_YCQL_USER" + + [[ -n "$YCQL_PASSWORD" ]] && ycql_password="$YCQL_PASSWORD" + + if [[ -z "$YCQL_PASSWORD" ]] && [[ "$ycql_user" != "$DEFAULT_YCQL_USER" ]]; then + ycql_password="$YCQL_USER" + fi + + [[ -n "$YCQL_KEYSPACE" ]] && ycql_keyspace="$YCQL_KEYSPACE" + + [[ -z "$YCQL_KEYSPACE" ]] && [[ -n "$YCQL_USER" ]] && ycql_keyspace="$ycql_user" + + api="ycql" + } + + create_user() { + declare -a ysql_cmd + declare -a ycql_cmd + + case "$api" in + "ysql") + export PGPASSWORD="$DEFAULT_YSQL_PASSWORD" + read -r -a ysql_cmd <<< "${prefix_ysql_cmd[@]}" + ysql_cmd+=( + -U "$DEFAULT_YSQL_USER" + -c "CREATE ROLE ${ysql_user} with LOGIN SUPERUSER password '${ysql_password}' ;" + -c "ALTER DATABASE ${ysql_db} OWNER TO ${ysql_user} ;" + ) + "${ysql_cmd[@]}" + ;; + "ycql") + read -r -a ycql_cmd <<< "${prefix_ycql_cmd[@]}" + ycql_cmd+=( + -u "$DEFAULT_YCQL_USER" + -p "$DEFAULT_YCQL_PASSWORD" + -e "CREATE ROLE IF NOT EXISTS ${ycql_user} WITH PASSWORD = '${ycql_password}' AND LOGIN = true AND SUPERUSER = true ;" + ) + "${ycql_cmd[@]}" + ;; + *) exit 1 + esac + } + + update_password() { + declare -a ysql_cmd + declare -a ycql_cmd + + case "$api" in + "ysql") + export PGPASSWORD="$DEFAULT_YSQL_PASSWORD" + read -r -a ysql_cmd <<< "${prefix_ysql_cmd[@]}" + ysql_cmd+=( + -U "$DEFAULT_YSQL_USER" + -c "ALTER ROLE ${ysql_user} WITH PASSWORD '${ysql_password}' ;" + ) + "${ysql_cmd[@]}" + ;; + "ycql") + read -r -a ycql_cmd <<< "${prefix_ycql_cmd[@]}" + ycql_cmd+=( + -u "$DEFAULT_YCQL_USER" + -p "$DEFAULT_YCQL_PASSWORD" + -e "ALTER ROLE ${ycql_user} WITH PASSWORD = '${ycql_password}' ;" + ) + "${ycql_cmd[@]}" + ;; + *) exit 1 + esac + } + + create_container() { + declare -a ysql_cmd + declare -a ycql_cmd + + case "$api" in + "ysql") + export PGPASSWORD="$DEFAULT_YSQL_PASSWORD" + read -r -a ysql_cmd <<< "${prefix_ysql_cmd[@]}" + ysql_cmd+=( + -U "$DEFAULT_YSQL_USER" + -c "CREATE DATABASE ${ysql_db} ;" + ) + "${ysql_cmd[@]}" + ;; + "ycql") + read -r -a ycql_cmd <<< "${prefix_ycql_cmd[@]}" + ycql_cmd+=( + -u "$DEFAULT_YCQL_USER" + -p "$DEFAULT_YCQL_PASSWORD" + -e "CREATE KEYSPACE IF NOT EXISTS ${ycql_keyspace} ;" + ) + "${ycql_cmd[@]}" + ;; + *) exit 1 + esac + } + + # ----------------------------------------- + # Main + # ----------------------------------------- + + trap cleanup EXIT + + echo "Waiting for YugabyteDB to start." + if ! timeout 3m bash -c "waitUntilHealthy ${DEFAULT_YSQL_USER} ${DEFAULT_YSQL_PASSWORD} ${YSQL_PORT} {{ .Values.tls.enabled }}"; then + echo "Timeout while waiting for database" + exit 1 + fi + + # YSQL Credentials + get_ysql_credentials + + ## Create YSQL DB + if [[ -n $ysql_db ]] && [[ "$ysql_db" != "$DEFAULT_YSQL_DB" ]]; then + create_container + fi + + ## Update YSQL Password + if [[ -n $ysql_password ]] && [[ "$ysql_password" != "$DEFAULT_YSQL_PASSWORD" ]] && [[ "$ysql_user" == "$DEFAULT_YSQL_USER" ]]; then + update_password + fi + + ## Create YSQL User + if [[ -n $ysql_user ]] && [[ "$ysql_user" != "$DEFAULT_YSQL_USER" ]]; then + create_user + fi + + # YCQL Credentials + get_ycql_credentials + + ## Create YCQL Keyspace + if [[ -n $ycql_keyspace ]] && [[ -n "$ycql_keyspace" ]]; then + create_container + fi + + ## Update YCQL Password + if [[ -n $ycql_password ]] && [[ "$ycql_password" != "$DEFAULT_YCQL_PASSWORD" ]] && [[ "$ycql_user" == "$DEFAULT_YCQL_USER" ]]; then + update_password + fi + + ## Create YCQL User + if [[ -n $ycql_user ]] && [[ "$ycql_user" != "$DEFAULT_YCQL_USER" ]]; then + create_user + fi + +{{- end }} diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/tserver-gflags-secret.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/tserver-gflags-secret.yaml new file mode 100644 index 0000000..85ea67b --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/tserver-gflags-secret.yaml @@ -0,0 +1,73 @@ +{{- $root := . -}} +{{- range $service := $root.Values.Services }} +{{- if eq $service.name "yb-tservers" }} +{{- $serviceValues := (dict "Service" $service "Values" $root.Values "Chart" $root.Chart "Release" $root.Release) -}} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "yugabyte.fullname" $root }}-tserver-gflags + namespace: "{{ $root.Release.Namespace }}" +type: Opaque +stringData: + server.conf.template: | +{{- if not $root.Values.storage.ephemeral }} + --fs_data_dirs={{ template "yugabyte.fs_data_dirs" $root.Values.storage.tserver }} +{{- else }} + --fs_data_dirs=/var/yugabyte +{{- end }} +{{- if eq $root.Values.ip_version_support "v6_only" }} + --net_address_filter=ipv6_external,ipv6_non_link_local,ipv6_all,ipv4_external,ipv4_all +{{- end }} +{{- if $root.Values.isMultiAz }} + --tserver_master_addrs={{ $root.Values.masterAddresses }} +{{- else }} + --tserver_master_addrs={{ template "yugabyte.master_addresses" $root }} +{{- end }} +{{- if or $root.Values.authCredentials.ysql.password $root.Values.authCredentials.ysql.passwordSecretName }} + --ysql_enable_auth=true + {{- if (include "yugabyte.tserver.readinessProbe" $root) }} + --ysql_hba_conf_csv="local all yugabyte trust" + {{- end }} +{{- end }} +{{- if or $root.Values.authCredentials.ycql.user $root.Values.authCredentials.ycql.password $root.Values.authCredentials.ycql.passwordSecretName }} + --use_cassandra_authentication=true +{{- end }} +{{- if $root.Values.tls.enabled }} + --certs_dir=/opt/certs/yugabyte + --use_node_to_node_encryption={{ $root.Values.tls.nodeToNode }} + --allow_insecure_connections={{ $root.Values.tls.insecure }} + --use_client_to_server_encryption={{ $root.Values.tls.clientToServer }} + --certs_for_client_dir=/opt/certs/yugabyte + {{- if $root.Values.tserver.serverBroadcastAddress }} + --cert_node_filename={{ include "yugabyte.server_fqdn" $serviceValues }} + {{- end }} +{{- end }} +{{- if not $root.Values.disableYsql }} + --enable_ysql=true + --pgsql_proxy_bind_address={{ include "yugabyte.pgsql_proxy_bind_address" $serviceValues }} +{{- else }} + --enable_ysql=false +{{- end }} +{{- if $root.Values.yugabytedUi.enabled }} + --tserver_enable_metrics_snapshotter={{ $root.Values.yugabytedUi.metricsSnapshotter.enabled }} + --metrics_snapshotter_interval_ms={{ $root.Values.yugabytedUi.metricsSnapshotter.interval }} + --metrics_snapshotter_tserver_metrics_whitelist={{ join "," $root.Values.yugabytedUi.metricsSnapshotter.whitelist }} +{{- end }} + --metric_node_name=${EXPORTED_INSTANCE} + --memory_limit_hard_bytes={{ template "yugabyte.memory_hard_limit" dict "size" $root.Values.resource.tserver.limits.memory "limitPercent" $root.Values.tserver.memoryLimitHardPercentage }} + --stderrthreshold=0 + --max_log_size=256 + --num_cpus={{ ceil $root.Values.resource.tserver.requests.cpu }} + --undefok=num_cpus,enable_ysql + --use_node_hostname_for_local_tserver=true + --cql_proxy_bind_address={{ include "yugabyte.cql_proxy_bind_address" $serviceValues }} + --rpc_bind_addresses={{ include "yugabyte.rpc_bind_address" $serviceValues }} + --server_broadcast_addresses={{ $root.Values.tserver.serverBroadcastAddress | default (include "yugabyte.server_broadcast_address" $serviceValues) }} + --webserver_interface={{ include "yugabyte.webserver_interface" $serviceValues }} +{{- range $flag, $override := $root.Values.gflags.tserver }} + --{{ $flag }}={{ $override }} +{{- end }} +{{- end }} +{{- end }} +--- diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/tserver-servicemonitor.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/tserver-servicemonitor.yaml new file mode 100644 index 0000000..64b8264 --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/templates/tserver-servicemonitor.yaml @@ -0,0 +1,115 @@ +{{- $sm := .Values.serviceMonitor }} +{{ if and $sm.enabled (or $sm.tserver.enabled $sm.ycql.enabled $sm.ysql.enabled $sm.yedis.enabled) }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "yugabyte.fullname" . }}-yb-tserver + labels: + {{- if .Values.oldNamingStyle }} + app: "yb-tserver" + {{- else }} + app.kubernetes.io/name: "yb-tserver" + {{- end }} + release: {{ .Release.Name | quote }} + chart: "{{ .Chart.Name }}" + component: "{{ .Values.Component }}" + {{- with .Values.serviceMonitor.extraLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + jobLabel: "release" + selector: + matchLabels: + {{- if .Values.oldNamingStyle }} + app: "yb-tserver" + {{- else }} + app.kubernetes.io/name: "yb-tserver" + {{- end }} + release: {{ .Release.Name | quote }} + service-type: "headless" + endpoints: + + {{- with .Values.serviceMonitor.tserver }} + {{- if .enabled }} + - port: {{ .port }} + path: {{ .path }} + {{- if .interval }} + interval: {{ .interval }} + {{- else }} + interval: {{ $.Values.serviceMonitor.interval }} + {{- end }} + relabelings: + - targetLabel: "group" + replacement: "yb-tserver" + - targetLabel: "export_type" + replacement: "tserver_export" + - targetLabel: "node_prefix" + replacement: {{ $.Release.Name | quote }} + metricRelabelings: + {{- toYaml $.Values.serviceMonitor.commonMetricRelabelings | nindent 4 }} + {{- end }} + {{- end }} + + {{- with .Values.serviceMonitor.ycql }} + {{- if .enabled }} + - port: {{ .port }} + path: {{ .path }} + {{- if .interval }} + interval: {{ .interval }} + {{- else }} + interval: {{ $.Values.serviceMonitor.interval }} + {{- end }} + relabelings: + - targetLabel: "group" + replacement: "ycql" + - targetLabel: "export_type" + replacement: "cql_export" + - targetLabel: "node_prefix" + replacement: {{ $.Release.Name | quote }} + metricRelabelings: + {{- toYaml $.Values.serviceMonitor.commonMetricRelabelings | nindent 4 }} + {{- end }} + {{- end }} + + {{- with .Values.serviceMonitor.ysql }} + {{- if .enabled }} + - port: {{ .port }} + path: {{ .path }} + {{- if .interval }} + interval: {{ .interval }} + {{- else }} + interval: {{ $.Values.serviceMonitor.interval }} + {{- end }} + relabelings: + - targetLabel: "group" + replacement: "ysql" + - targetLabel: "export_type" + replacement: "ysql_export" + - targetLabel: "node_prefix" + replacement: {{ $.Release.Name | quote }} + metricRelabelings: + {{- toYaml $.Values.serviceMonitor.commonMetricRelabelings | nindent 4 }} + {{- end }} + {{- end }} + + {{- with .Values.serviceMonitor.yedis }} + {{- if .enabled }} + - port: {{ .port }} + path: {{ .path }} + {{- if .interval }} + interval: {{ .interval }} + {{- else }} + interval: {{ $.Values.serviceMonitor.interval }} + {{- end }} + relabelings: + - targetLabel: "group" + replacement: "yedis" + - targetLabel: "export_type" + replacement: "redis_export" + - targetLabel: "node_prefix" + replacement: {{ $.Release.Name | quote }} + metricRelabelings: + {{- toYaml $.Values.serviceMonitor.commonMetricRelabelings | nindent 4 }} + {{- end }} + {{- end }} +{{- end }} diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/values.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/values.yaml new file mode 100644 index 0000000..080c17a --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/values.yaml @@ -0,0 +1,886 @@ +# Default values for yugabyte. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +Component: "yugabytedb" + +fullnameOverride: "" +nameOverride: "" + +Image: + repository: "yugabytedb/yugabyte" + tag: 2025.2.1.0-b141 + pullPolicy: IfNotPresent + pullSecretName: "" + +storage: + ephemeral: false # will not allocate PVs when true + master: + count: 2 + size: 10Gi + storageClass: "" + tserver: + count: 2 + size: 10Gi + storageClass: "" + +resource: + master: + requests: + cpu: "2" + memory: 2Gi + limits: + cpu: "2" + ## Ensure the 'memory' value is strictly in 'Gi' or 'G' format. Deviating from these formats + ## may result in setting an incorrect value for the 'memory_limit_hard_bytes' flag. + ## Avoid using floating numbers for the numeric part of 'memory'. Doing so may lead to + ## the 'memory_limit_hard_bytes' being set to 0, as the function expects integer values. + memory: 2Gi + tserver: + requests: + cpu: "2" + memory: 4Gi + limits: + cpu: "2" + ## Ensure the 'memory' value is strictly in 'Gi' or 'G' format. Deviating from these formats + ## may result in setting an incorrect value for the 'memory_limit_hard_bytes' flag. + ## Avoid using floating numbers for the numeric part of 'memory'. Doing so may lead to + ## the 'memory_limit_hard_bytes' being set to 0, as the function expects integer values. + memory: 4Gi + +replicas: + master: 3 + tserver: 3 + ## Used to set replication factor when isMultiAz is set to true + totalMasters: 3 + +partition: + master: 0 + tserver: 0 + +updateStrategy: + type: RollingUpdate + +# Used in Multi-AZ setup +masterAddresses: "" + +isMultiAz: false +AZ: "" + +# Disable the YSQL +disableYsql: false + +tls: + # Set to true to enable the TLS. + enabled: false + nodeToNode: true + clientToServer: true + # Set to false to disallow any service with unencrypted communication from joining this cluster + insecure: false + # Set enabled to true to use cert-manager instead of providing your own rootCA + certManager: + enabled: false + # Will create own ca certificate and issuer when set to true + bootstrapSelfsigned: true + # Use ClusterIssuer when set to true, otherwise use Issuer + useClusterIssuer: false + # Name of ClusterIssuer to use when useClusterIssuer is true + clusterIssuer: cluster-ca + # Name of Issuer to use when useClusterIssuer is false + issuer: yugabyte-ca + + # Set the below value to true for using custom issuer like AWSPCAClusterIssuer + useCustomIssuer: false + # if customIssuer is set to true, then below issuer values will get applied + customIssuer: + kind: Issuer + name: yugabyte-ca + group: cert-manager.io + + certificates: + # The lifetime before cert-manager will issue a new certificate. + # The re-issued certificates will not be automatically reloaded by the service. + # It is necessary to provide some external means of restarting the pods. + duration: 2160h # 90d + renewBefore: 360h # 15d + algorithm: RSA # ECDSA or RSA + # Can be 2048, 4096 or 8192 for RSA + # Or 256, 384 or 521 for ECDSA + keySize: 2048 + # Enable this option to include a commonName in the Certificate. + # Certain custom Certificate Authorities (CAs), such as AWS PCA, require the commonName + # field to be present in the certificate for successful issuance. + # If set to true, the commonName of certs will automatically be set to the service name. + commonNameRequired: false + # If commonNameSuffix is specified, it will be used as the suffix for the commonName for the certificate. + commonNameSuffix: "" + + ## When certManager.enabled=false, rootCA.cert and rootCA.key are used to generate TLS certs. + ## When certManager.enabled=true and boostrapSelfsigned=true, rootCA is ignored. + ## When certManager.enabled=true and bootstrapSelfsigned=false, only rootCA.cert is used + ## to verify TLS certs generated and signed by the external provider. + rootCA: + cert: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM2VENDQWRHZ0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBREFXTVJRd0VnWURWUVFERXd0WmRXZGgKWW5sMFpTQkVRakFlRncweE9UQXlNRGd3TURRd01qSmFGdzB5T1RBeU1EVXdNRFF3TWpKYU1CWXhGREFTQmdOVgpCQU1UQzFsMVoyRmllWFJsSUVSQ01JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBCnVOMWF1aWc4b2pVMHM0OXF3QXhrT2FCaHkwcTlyaVg2akVyZWJyTHJOWDJOeHdWQmNVcWJkUlhVc3VZNS96RUQKUC9CZTNkcTFuMm9EQ2ZGVEwweGkyNFdNZExRcnJBMndCdzFtNHM1WmQzcEJ1U04yWHJkVVhkeUx6dUxlczJNbgovckJxcWRscXp6LzAyTk9TOE9SVFZCUVRTQTBSOFNMQ1RjSGxMQmRkMmdxZ1ZmemVXRlVObXhWQ2EwcHA5UENuCmpUamJJRzhJWkh5dnBkTyt3aURQM1Y1a1ZEaTkvbEtUaGUzcTFOeDg5VUNFcnRJa1pjSkYvWEs3aE90MU1sOXMKWDYzb2lVMTE1Q2svbGFGRjR6dWgrZk9VenpOVXRXeTc2RE92cm5pVGlaU0tQZDBBODNNa2l2N2VHaDVkV3owWgpsKzJ2a3dkZHJaRzVlaHhvbGhGS3pRSURBUUFCbzBJd1FEQU9CZ05WSFE4QkFmOEVCQU1DQXFRd0hRWURWUjBsCkJCWXdGQVlJS3dZQkJRVUhBd0VHQ0NzR0FRVUZCd01DTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3RFFZSktvWkkKaHZjTkFRRUxCUUFEZ2dFQkFEQjVRbmlYd1ptdk52eG5VbS9sTTVFbms3VmhTUzRUZldIMHY4Q0srZWZMSVBTbwpVTkdLNXU5UzNEUWlvaU9SN1Vmc2YrRnk1QXljMmNUY1M2UXBxTCt0V1QrU1VITXNJNk9oQ05pQ1gvQjNKWERPCkd2R0RIQzBVOHo3aWJTcW5zQ2Rid05kajAyM0lwMHVqNE9DVHJ3azZjd0RBeXlwVWkwN2tkd28xYWJIWExqTnAKamVQMkwrY0hkc2dKM1N4WWpkK1kvei9IdmFrZG1RZDJTL1l2V0R3aU1SRDkrYmZXWkJVRHo3Y0QyQkxEVmU0aAp1bkFaK3NyelR2Sjd5dkVodzlHSDFyajd4Qm9VNjB5SUUrYSszK2xWSEs4WnBSV0NXMnh2eWNrYXJSKytPS2NKClFsL04wWExqNWJRUDVoUzdhOTdhQktTamNqY3E5VzNGcnhJa2tKST0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=" + key: "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBdU4xYXVpZzhvalUwczQ5cXdBeGtPYUJoeTBxOXJpWDZqRXJlYnJMck5YMk54d1ZCCmNVcWJkUlhVc3VZNS96RURQL0JlM2RxMW4yb0RDZkZUTDB4aTI0V01kTFFyckEyd0J3MW00czVaZDNwQnVTTjIKWHJkVVhkeUx6dUxlczJNbi9yQnFxZGxxenovMDJOT1M4T1JUVkJRVFNBMFI4U0xDVGNIbExCZGQyZ3FnVmZ6ZQpXRlVObXhWQ2EwcHA5UENualRqYklHOElaSHl2cGRPK3dpRFAzVjVrVkRpOS9sS1RoZTNxMU54ODlVQ0VydElrClpjSkYvWEs3aE90MU1sOXNYNjNvaVUxMTVDay9sYUZGNHp1aCtmT1V6ek5VdFd5NzZET3ZybmlUaVpTS1BkMEEKODNNa2l2N2VHaDVkV3owWmwrMnZrd2RkclpHNWVoeG9saEZLelFJREFRQUJBb0lCQUJsdW1tU3gxR1djWER1Mwpwei8wZEhWWkV4c2NsU3U0SGRmZkZPcTF3cFlCUjlmeGFTZGsxQzR2YXF1UjhMaWl6WWVtVWViRGgraitkSnlSCmpwZ2JNaDV4S1BtRkw5empwU3ZUTkN4UHB3OUF5bm5sM3dyNHZhcU1CTS9aZGpuSGttRC9kQzBadEEvL0JIZ3YKNHk4d3VpWCsvUWdVaER0Z1JNcmR1ZUZ1OVlKaFo5UE9jYXkzSkkzMFhEYjdJSS9vNFNhYnhTcFI3bTg5WjY0NwpUb3hsOEhTSzl0SUQxbkl1bHVpTmx1dHI1RzdDdE93WTBSc2N5dmZ2elg4a1d2akpLZVJVbmhMSCtXVFZOaExICjdZc0tMNmlLa1NkckMzeWVPWnV4R0pEbVdrZVgxTzNPRUVGYkc4TjVEaGNqL0lXbDh1dGt3LzYwTEthNHBCS2cKTXhtNEx3RUNnWUVBNnlPRkhNY2pncHYxLzlHZC8yb3c2YmZKcTFjM1dqQkV2cnM2ZXNyMzgrU3UvdVFneXJNcAo5V01oZElpb2dYZjVlNjV5ZlIzYVBXcjJJdWMxZ0RUNlYycDZFR2h0NysyQkF1YkIzczloZisycVNRY1lkS3pmCnJOTDdKalE4ZEVGZWdYd041cHhKOTRTTVFZNEI4Qm9hOHNJWTd3TzU4dHpVMjZoclVnanFXQ1VDZ1lFQXlVUUIKNzViWlh6MGJ5cEc5NjNwYVp0bGlJY0cvUk1XMnVPOE9rVFNYSGdDSjBob25uRm5IMGZOc1pGTHdFWEtnTTRORworU3ZNbWtUekE5eVVSMHpIMFJ4UW44L1YzVWZLT2k5RktFeWx6NzNiRkV6ZW1QSEppQm12NWQ4ZTlOenZmU0E0CkdpRTYrYnFyV3VVWWRoRWlYTnY1SFNPZ3I4bUx1TzJDbGlmNTg0a0NnWUFlZzlDTmlJWmlOODAzOHNNWFYzZWIKalI5ZDNnYXY3SjJ2UnVyeTdvNDVGNDlpUXNiQ3AzZWxnY1RnczY5eWhkaFpwYXp6OGNEVndhREpyTW16cHF4cQpWY1liaFFIblppSWM5MGRubS9BaVF2eWJWNUZqNnQ5b05VVWtreGpaV1haalJXOGtZMW55QmtDUmJWVnhER0k4CjZOV0ZoeTFGaUVVVGNJcms3WVZFQlFLQmdRREpHTVIrYWRFamtlRlUwNjVadkZUYmN0VFVPY3dzb1Foalc2akkKZVMyTThxakNYeE80NnhQMnVTeFNTWFJKV3FpckQ3NDRkUVRvRjRCaEdXS21veGI3M3pqSGxWaHcwcXhDMnJ4VQorZENxODE0VXVJR3BlOTBMdWU3QTFlRU9kRHB1WVdUczVzc1FmdTE3MG5CUWQrcEhzaHNFZkhhdmJjZkhyTGpQCjQzMmhVUUtCZ1FDZ3hMZG5Pd2JMaHZLVkhhdTdPVXQxbGpUT240SnB5bHpnb3hFRXpzaDhDK0ZKUUQ1bkFxZXEKZUpWSkNCd2VkallBSDR6MUV3cHJjWnJIN3IyUTBqT2ZFallwU1dkZGxXaWh4OTNYODZ0aG83UzJuUlYrN1hNcQpPVW9ZcVZ1WGlGMWdMM1NGeHZqMHhxV3l0d0NPTW5DZGFCb0M0Tkw3enJtL0lZOEUwSkw2MkE9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=" + ## When tls.certManager.enabled=false + ## nodeCert and clientCert will be used only when rootCA.key is empty. + ## Will be ignored and genSignedCert will be used to generate + ## node and client certs if rootCA.key is provided. + ## cert and key are base64 encoded content of certificate and key. + nodeCert: + cert: "" + key: "" + clientCert: + cert: "" + key: "" + ## Checksum for CA certificate. If set to empty, statefulset + ## will calculate new checksum value and decide whether to restart pods + ## based on the result of comparing previous checksum value and new value. + ## If previous value is provided again, there will be no Pod restart + ## for CA certificate changes. + rootCAChecksum: "" + +gflags: + master: + default_memory_limit_to_ram_ratio: 0.85 + tserver: {} +# use_cassandra_authentication: false + ybc: {} + +yugabytedUi: + enabled: true + # If recoverWithoutFailure is true, yugabyted-ui will automatically try to restart itself + # instead of failing and causing the pod to restart + recoverWithoutFailure: true + recoveryInterval: 30s + # metricsSnapshotter must be enabled for yugabytedUi to properly display metrics + metricsSnapshotter: + enabled: true + # time between each metric snapshot in ms + interval: 11000 + whitelist: + - handler_latency_yb_tserver_TabletServerService_Read_count + - handler_latency_yb_tserver_TabletServerService_Write_count + - handler_latency_yb_tserver_TabletServerService_Read_sum + - handler_latency_yb_tserver_TabletServerService_Write_sum + - disk_usage + - cpu_usage + - node_up + +PodManagementPolicy: Parallel + +enableLoadBalancer: true + +ybc: + enabled: false + # Use inbuilt YB-Controller executable + useYBDBImage: false + ## https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-requests-and-limits-of-pod-and-container + ## Use the above link to learn more about Kubernetes resources configuration. + resources: + requests: + cpu: "1" + memory: 1Gi + limits: + cpu: "1" + memory: 1Gi + ## Checksum for controller gflags secret. If set to empty, statefulset + ## will calculate new checksum value and decide whether to restart pods + ## based on the result of comparing previous checksum value and new value. + ## If previous value is provided again, there will be no Pod restart + ## for gflags changes. + gflagsChecksum: "" + +ybCleanup: + ## https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-requests-and-limits-of-pod-and-container + ## Use the above link to learn more about Kubernetes resources configuration. + resources: + requests: + cpu: "0.25" + memory: 250Mi + limits: + cpu: "0.25" + memory: 250Mi + +domainName: "cluster.local" + +# ## Enable global service endpoints for tserver/master services across +# ## zones. Requires common label "app.kubernetes.io/part-of" on resources. +# globalServiceEndpoints: +# tserverGlobalService: +# enabled: false +# # Will use LoadBalancer only if 'enableLoadBalancer' is true. +# # Otherwise, ClusterIP service is created. +# type: LoadBalancer +# annotations: {} +# ## Sets the Service's externalTrafficPolicy +# externalTrafficPolicy: "" +# app: "yb-tserver" +# loadBalancerIP: "" +# name: "tserver-global-service" +# ports: +# tcp-yql-port: "9042" +# tcp-yedis-port: "6379" +# tcp-ysql-port: "5433" + +# masterGlobalService: +# enabled: false +# # Will use LoadBalancer only if 'enableLoadBalancer' is true. +# # Otherwise, ClusterIP service is created. +# type: LoadBalancer +# annotations: {} +# ## Sets the Service's externalTrafficPolicy +# externalTrafficPolicy: "" +# app: "yb-master" +# loadBalancerIP: "" +# name: "master-global-service" +# ports: +# http-ui: "7000" + +## If you would like to define which IP family to use for single stack or define +## the order of IP families for dual-stack, you can choose the address families by +## setting the ipFamilies. It can be any of these values: +## ["IPv4"] +## ["IPv6"] +## ["IPv4","IPv6"] (dual stack) +## ["IPv6","IPv4"] (dual stack) +## Docs: https://kubernetes.io/docs/concepts/services-networking/dual-stack/#dual-stack-service-configuration-scenarios +ipFamilies: [] + +## ipFamilyPolicy can be set to the following values: +## SingleStack +## PreferDualStack +## RequireDualStack +## Selects the .spec.clusterIP from the list of .spec.clusterIPs based on the address +## family of the first element in the .spec.ipFamilies array. +## Docs: https://kubernetes.io/docs/concepts/services-networking/dual-stack/#dual-stack-service-configuration-scenarios +ipFamilyPolicy: "" + +## Default service scope to use if not defined in service endpoint definition. +## Can be "Namespaced"/"AZ" +defaultServiceScope: "AZ" + +serviceEndpoints: + - name: "yb-master-ui" + type: LoadBalancer + # Can be AZ/Namespaced + scope: "AZ" + annotations: {} + clusterIP: "" + ## Sets the Service's externalTrafficPolicy + externalTrafficPolicy: "" + app: "yb-master" + loadBalancerIP: "" + ports: + http-ui: "7000" + extraArgs: {} + + - name: "yb-tserver-service" + type: LoadBalancer + # Can be AZ/Namespaced + scope: "AZ" + annotations: {} + clusterIP: "" + ## Sets the Service's externalTrafficPolicy + externalTrafficPolicy: "" + app: "yb-tserver" + loadBalancerIP: "" + ports: + tcp-yql-port: "9042" + tcp-yedis-port: "6379" + tcp-ysql-port: "5433" + extraArgs: {} + + - name: "yugabyted-ui-service" + type: LoadBalancer + # Can be AZ/Namespaced + scope: "AZ" + annotations: {} + clusterIP: "" + ## Sets the Service's externalTrafficPolicy + externalTrafficPolicy: "" + app: "yb-master" + loadBalancerIP: "" + sessionAffinity: ClientIP + ports: + yugabyted-ui: "15433" + extraArgs: {} + +Services: + - name: "yb-masters" + label: "yb-master" + skipHealthChecks: false + memory_limit_to_ram_ratio: 0.85 + ports: + http-ui: "7000" + tcp-rpc-port: "7100" + yugabyted-ui: "15433" + + - name: "yb-tservers" + label: "yb-tserver" + skipHealthChecks: false + ports: + http-ui: "9000" + tcp-rpc-port: "9100" + tcp-yql-port: "9042" + tcp-yedis-port: "6379" + tcp-ysql-port: "5433" + http-ycql-met: "12000" + http-yedis-met: "11000" + http-ysql-met: "13000" + tcp-ybc-port: "18018" + yugabyted-ui: "15433" + + +## Should be set to true only if Istio is being used. This also adds +## the Istio sidecar injection labels to the pods. +## TODO: remove this once +## https://github.com/yugabyte/yugabyte-db/issues/5641 is fixed. +## +istioCompatibility: + enabled: false + +## Settings required when using multicluster environment. +multicluster: + ## Creates a ClusterIP service for each yb-master and yb-tserver + ## pod. + createServicePerPod: false + ## creates a ClusterIP service whos name does not have release name + ## in it. A common service across different clusters for automatic + ## failover. Useful when using new naming style. + createCommonTserverService: false + + ## Enable it to deploy YugabyteDB in a multi-cluster services enabled + ## Kubernetes cluster (KEP-1645). This will create ServiceExport. + ## GKE Ref - https://cloud.google.com/kubernetes-engine/docs/how-to/multi-cluster-services#registering_a_service_for_export + ## You can use this gist for the reference to deploy the YugabyteDB in a multi-cluster scenario. + ## Gist - https://gist.github.com/baba230896/78cc9bb6f4ba0b3d0e611cd49ed201bf + createServiceExports: false + + ## Mandatory variable when createServiceExports is set to true. + ## Use: In case of GKE, you need to pass GKE Hub Membership Name. + ## GKE Ref - https://cloud.google.com/kubernetes-engine/docs/how-to/multi-cluster-services#enabling + kubernetesClusterId: "" + + ## mcsApiVersion is used for the MCS resources created by the + ## chart. Set to net.gke.io/v1 when using GKE MCS. + mcsApiVersion: "multicluster.x-k8s.io/v1alpha1" + +serviceMonitor: + ## If true, two ServiceMonitor CRs are created. One for yb-master + ## and one for yb-tserver + ## https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#servicemonitor + ## + enabled: false + ## interval is the default scrape_interval for all the endpoints + interval: 30s + ## extraLabels can be used to add labels to the ServiceMonitors + ## being created + extraLabels: {} + # release: prom + + ## Configurations of ServiceMonitor for yb-master + master: + enabled: true + port: "http-ui" + interval: "" + path: "/prometheus-metrics" + + ## Configurations of ServiceMonitor for yb-tserver + tserver: + enabled: true + port: "http-ui" + interval: "" + path: "/prometheus-metrics" + ycql: + enabled: true + port: "http-ycql-met" + interval: "" + path: "/prometheus-metrics" + ysql: + enabled: true + port: "http-ysql-met" + interval: "" + path: "/prometheus-metrics" + yedis: + enabled: true + port: "http-yedis-met" + interval: "" + path: "/prometheus-metrics" + + commonMetricRelabelings: + # https://git.io/JJW5p + # Save the name of the metric so we can group_by since we cannot by __name__ directly... + - sourceLabels: ["__name__"] + regex: "(.*)" + targetLabel: "saved_name" + replacement: "$1" + # The following basically retrofit the handler_latency_* metrics to label format. + - sourceLabels: ["__name__"] + regex: "handler_latency_(yb_[^_]*)_([^_]*)_([^_]*)(.*)" + targetLabel: "server_type" + replacement: "$1" + - sourceLabels: ["__name__"] + regex: "handler_latency_(yb_[^_]*)_([^_]*)_([^_]*)(.*)" + targetLabel: "service_type" + replacement: "$2" + - sourceLabels: ["__name__"] + regex: "handler_latency_(yb_[^_]*)_([^_]*)_([^_]*)(_sum|_count)?" + targetLabel: "service_method" + replacement: "$3" + - sourceLabels: ["__name__"] + regex: "handler_latency_(yb_[^_]*)_([^_]*)_([^_]*)(_sum|_count)?" + targetLabel: "__name__" + replacement: "rpc_latency$4" + +resources: {} + +nodeSelector: {} + +affinity: {} + +statefulSetAnnotations: {} + +networkAnnotation: {} + +commonLabels: {} + +## @param dnsPolicy DNS Policy for pod +## ref: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/ +## E.g. +## dnsPolicy: ClusterFirst +dnsPolicy: "" +## @param dnsConfig DNS Configuration pod +## ref: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/ +## E.g. +## dnsConfig: +## options: +## - name: ndots +## value: "4" +dnsConfig: {} + + +master: + ## Ref: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#affinity-v1-core + ## This might override the default affinity from service.yaml + # To successfully merge, we need to follow rules for merging nodeSelectorTerms that kubernentes + # has. Each new node selector term is ORed together, and each match expression or match field in + # a single selector is ANDed together. + # This means, if a pod needs to be scheduled on a label 'custom_label_1' with a value + # 'custom_value_1', we need to add this 'subterm' to each of our pre-defined node affinity + # terms. + # + # Pod anti affinity is a simpler merge. Each term is applied separately, and the weight is tracked. + # The pod that achieves the highest weight is selected. + ## Example. + # affinity: + # podAntiAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # - labelSelector: + # matchExpressions: + # - key: app + # operator: In + # values: + # - "yb-master" + # topologyKey: kubernetes.io/hostname + # + # For further examples, see examples/yugabyte/affinity_overrides.yaml + affinity: {} + + ## Extra environment variables passed to the Master pods. + ## Ref: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#envvar-v1-core + ## Example: + # extraEnv: + # - name: NODE_IP + # valueFrom: + # fieldRef: + # fieldPath: status.hostIP + extraEnv: [] + + # secretEnv variables are used to expose secrets data as env variables in the master pod. + # TODO Add namespace also to support copying secrets from other namespace. + # secretEnv: + # - name: MYSQL_LDAP_PASSWORD + # valueFrom: + # secretKeyRef: + # name: secretName + # key: password + secretEnv: [] + + ## Annotations to be added to the Master pods. + podAnnotations: {} + + ## Labels to be added to the Master pods. + podLabels: {} + + ## Tolerations to be added to the Master pods. + ## Ref: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#toleration-v1-core + ## Example: + # tolerations: + # - key: dedicated + # operator: Equal + # value: experimental + # effect: NoSchedule + tolerations: [] + + ## Extra volumes + ## extraVolumesMounts are mandatory for each extraVolumes. + ## Ref: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#volume-v1-core + ## Example: + # extraVolumes: + # - name: custom-nfs-vol + # persistentVolumeClaim: + # claimName: some-nfs-claim + extraVolumes: [] + + ## Extra volume mounts + ## Ref: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#volumemount-v1-core + ## Example: + # extraVolumeMounts: + # - name: custom-nfs-vol + # mountPath: /home/yugabyte/nfs-backup + extraVolumeMounts: [] + + ## Set service account for master DB pods. The service account + ## should exist in the namespace where the master DB pods are brought up. + serviceAccount: "" + + ## Memory limit hard % (between 1-100) of the memory limit. + memoryLimitHardPercentage: 85 + + ## Readiness Probe + readinessProbe: + enabled: false + initialDelaySeconds: 30 + periodSeconds: 20 + timeoutSeconds: 10 + failureThreshold: 3 + successThreshold: 1 + + ## Custom readinessProbe that overrides the default one + ## Example: HTTP based Master readinessProbe + # customReadinessProbe: + # initialDelaySeconds: 30 + # periodSeconds: 20 + # timeoutSeconds: 10 + # failureThreshold: 3 + # successThreshold: 1 + # httpGet: + # path: / + # port: 7000 + ## Ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes + customReadinessProbe: {} + + ## Startup probe + startupProbe: + enabled: false + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 30 + successThreshold: 1 + + ## Custom startupProbe that overrides the default one + ## Enabling it will overrides the tserver.startupProbe + ## Example: HTTP based Master startupProbe + # customStartupProbe: + # initialDelaySeconds: 30 + # periodSeconds: 10 + # timeoutSeconds: 5 + # failureThreshold: 3 + # successThreshold: 1 + # httpGet: + # path: / + # port: 7000 + ## Ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-startup-probes + customStartupProbe: {} + + ## Checksum for master gflags secret. If set to empty, statefulset + ## will calculate new checksum value and decide whether to restart pods + ## based on the result of comparing previous checksum value and new value. + ## If previous value is provided again, there will be no Pod restart + ## for gflags changes. + gflagsChecksum: "" + + advanced: + ## Bash commands to run before the start of the master service + ## WARNING: This is an advanced feature. Incorrect use may cause startup failures, + ## security issues, or unexpected behavior. Use with caution and at your own risk. + preCommands: "" + + +tserver: + ## Ref: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#affinity-v1-core + ## This might override the default affinity from service.yaml + # To successfully merge, we need to follow rules for merging nodeSelectorTerms that kubernentes + # has. Each new node selector term is ORed together, and each match expression or match field in + # a single selector is ANDed together. + # This means, if a pod needs to be scheduled on a label 'custom_label_1' with a value + # 'custom_value_1', we need to add this 'subterm' to each of our pre-defined node affinity + # terms. + # + # Pod anti affinity is a simpler merge. Each term is applied separately, and the weight is tracked. + # The pod that achieves the highest weight is selected. + ## Example. + # affinity: + # podAntiAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # - labelSelector: + # matchExpressions: + # - key: app + # operator: In + # values: + # - "yb-tserver" + # topologyKey: kubernetes.io/hostname + # For further examples, see examples/yugabyte/affinity_overrides.yaml + affinity: {} + + ## Extra environment variables passed to the TServer pods. + ## Ref: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#envvar-v1-core + ## Example: + # extraEnv: + # - name: NODE_IP + # valueFrom: + # fieldRef: + # fieldPath: status.hostIP + extraEnv: [] + + ## secretEnv variables are used to expose secrets data as env variables in the tserver pods. + ## If namespace field is not specified we assume that user already + ## created the secret in the same namespace as DB pods. + ## Example + # secretEnv: + # - name: MYSQL_LDAP_PASSWORD + # valueFrom: + # secretKeyRef: + # name: secretName + # namespace: my-other-namespace-with-ldap-secret + # key: password + secretEnv: [] + + ## Annotations to be added to the TServer pods. + podAnnotations: {} + + ## Labels to be added to the TServer pods. + podLabels: {} + + ## Tolerations to be added to the TServer pods. + ## Ref: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#toleration-v1-core + ## Example: + # tolerations: + # - key: dedicated + # operator: Equal + # value: experimental + # effect: NoSchedule + tolerations: [] + + ## Sets the --server_broadcast_addresses flag on the TServer, no + ## preflight checks are done for this address. You might need to add + ## `use_private_ip: cloud` to the gflags.master and gflags.tserver. + serverBroadcastAddress: "" + + ## Extra volumes + ## extraVolumesMounts are mandatory for each extraVolumes. + ## Ref: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#volume-v1-core + ## Example: + # extraVolumes: + # - name: custom-nfs-vol + # persistentVolumeClaim: + # claimName: some-nfs-claim + extraVolumes: [] + + ## Extra volume mounts + ## Ref: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#volumemount-v1-core + ## Example: + # extraVolumeMounts: + # - name: custom-nfs-vol + # path: /home/yugabyte/nfs-backup + extraVolumeMounts: [] + + ## Set service account for tserver DB pods. The service account + ## should exist in the namespace where the tserver DB pods are brought up. + serviceAccount: "" + + ## Memory limit hard % (between 1-100) of the memory limit. + memoryLimitHardPercentage: 85 + + ## Readiness Probe + readinessProbe: + enabled: false + initialDelaySeconds: 30 + periodSeconds: 20 + timeoutSeconds: 10 + failureThreshold: 3 + successThreshold: 1 + + ## Custom readinessProbe that overrides the default one + ## Enabling it will overrides the tserver.readinessProbe + ## Example: HTTP based Tserver readinessProbe + # customReadinessProbe: + # initialDelaySeconds: 30 + # periodSeconds: 20 + # timeoutSeconds: 10 + # httpGet: + # path: / + # port: 9000 + ## Ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-readiness-probes + customReadinessProbe: {} + + ## Startup probe + startupProbe: + enabled: false + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 30 + successThreshold: 1 + + ## Custom startupProbe that overrides the default one + ## Enabling it will overrides the tserver.startupProbe + ## Example: HTTP based Tserver startupProbe + # customStartupProbe: + # initialDelaySeconds: 30 + # periodSeconds: 10 + # timeoutSeconds: 5 + # failureThreshold: 3 + # successThreshold: 1 + # httpGet: + # path: / + # port: 9000 + ## Ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-startup-probes + customStartupProbe: {} + + ## Checksum for tserver gflags secret. If set to empty, statefulset + ## will calculate new checksum value and decide whether to restart pods + ## based on the result of comparing previous checksum value and new value. + ## If previous value is provided again, there will be no Pod restart + ## for gflags changes. + gflagsChecksum: "" + + advanced: + ## Bash commands to run before the start of the tserver service + ## WARNING: This is an advanced feature. Incorrect use may cause startup failures, + ## security issues, or unexpected behavior. Use with caution and at your own risk. + preCommands: "" + +helm2Legacy: false + +ip_version_support: "v4_only" # v4_only, v6_only are the only supported values at the moment + +# For more https://docs.yugabyte.com/latest/reference/configuration/yugabyted/#environment-variables +authCredentials: + ysql: + user: "" + password: "" + database: "" + # Must contain the key ysqlPassword + passwordSecretName: "" + ycql: + user: "" + password: "" + keyspace: "" + # Must contain the key ycqlPassword + passwordSecretName: "" + +oldNamingStyle: true + +preflight: + # Set to true to skip disk IO check, DNS address resolution, and + # port bind checks + skipAll: false + # Set to true to skip port bind checks + skipBind: false + + ## Set to true to skip ulimit verification + ## SkipAll has higher priority + skipUlimit: false + +## Liveness Probe configuration for both master and tserver pods +livenessProbe: + enabled: true + failureThreshold: 3 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + +## Custom livenessProbe that overrides the default one +## Example: +# customLivenessProbe: +# failureThreshold: 3 +# periodSeconds: 10 +# timeoutSeconds: 1 +# successThreshold: 1 +# exec: +# command: +# - /custom/health/check +customLivenessProbe: {} + +## Pod securityContext +## Ref: https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#security-context +## The following configuration runs YB-Master and YB-TServer as a non-root user +podSecurityContext: + enabled: false + ## Mark it false, if you want to stop the non root user validation + runAsNonRoot: true + fsGroup: 10001 + runAsUser: 10001 + runAsGroup: 10001 + +## Added to handle old universe which has volume annotations +## K8s universe <= 2.5 to >= 2.6 +legacyVolumeClaimAnnotations: false + +useOldPodDisruptionBudget: true + +## Set to true to enable OCP compatibility mode. +ocpCompatibility: + enabled: false + +## Set to true to run OTEL collector as a sidecar in tserver pods +otelCollector: + enabled: false + image: "" + exporters: {} # Add exporters here + # exporters: + # datadog: + # api: + # key: + # site: datadoghq.com + # retry_on_failure: + # enabled: true + # sending_queue: + # enabled: true + # storage: file_storage/queue + + recievers: + ysql: + # Set ysql receiver params here, based on log_line_prefix set in ysql_pg.conf + lineStartPattern: "(?P\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}[.]\\d{3} \\w{3})[ ][[](?P\\d+)[]][ ]" + regex: "(?P\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}[.]\\d{3} \\w{3})[ ][[](?P\\d+)[]][ ](?P\\w+): AUDIT: (?P\\w+),(?P\\d+),(?P\\d+),(?P\\w+),(?P[^,]+),(?P[^,]*),(?P[^,]*),(?P(.|\\n|\\r|\\s)*)" + timestamp: + layout: '%Y-%m-%d %H:%M:%S.%L %Z' + parse_from: 'attributes.timestamp_with_ms' + resources: {} + # resources: + # requests: + # cpu: "100m" + # memory: "256Mi" + # limits: + # cpu: "200m" + # memory: "512Mi" + + # Add secret environment variables here if using AWS CloudWatch or GCP Log Explorer. + secretEnv: [] + # secretEnv: + # - envName: AWS_ACCESS_KEY_ID + # envValue: + # - envName: AWS_SECRET_ACCESS_KEY + # envValue: + # - envName: GOOGLE_APPLICATION_CREDENTIALS_CONTENT + # envValue: + + # Alternatively, set `tserver.serviceAccount` to a service account with the necessary permissions + # to utilize IAM roles for access instead of embedding credentials directly. + # Additionally, ensure that the appropriate nodeSelector is added to enable the metadata server + # for example: for IAM authentication in GKE. + # nodeSelector: + # iam.gke.io/gke-metadata-server-enabled: "true" diff --git a/yugabyte/charts/yugabyte-2025.2.1/yugabyte/yugabyte-rbac.yaml b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/yugabyte-rbac.yaml new file mode 100644 index 0000000..d021fbc --- /dev/null +++ b/yugabyte/charts/yugabyte-2025.2.1/yugabyte/yugabyte-rbac.yaml @@ -0,0 +1,19 @@ +# Create YugaByte specific service account +apiVersion: v1 +kind: ServiceAccount +metadata: + name: yugabyte-helm + namespace: kube-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: yugabyte-helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - kind: ServiceAccount + name: yugabyte-helm + namespace: kube-system diff --git a/yugabyte/kustomization.yaml b/yugabyte/kustomization.yaml new file mode 100644 index 0000000..6c8b97a --- /dev/null +++ b/yugabyte/kustomization.yaml @@ -0,0 +1,36 @@ +# # /db/kustomization.yaml +# apiVersion: kustomize.config.k8s.io/v1beta1 +# kind: Kustomization + +# namespace: db # Forces everything in this folder into the 'db' namespace + +# # resources: +# # - yugabytedb2.yaml # Your StatefulSet/Service file generated via helm template + +# # using Kustomize way +# helmCharts: +# - name: yugabyte +# repo: https://charts.yugabyte.com +# version: 2025.2.1 # Always pin your version! +# #version: 2.18.0 # Specify the version you want +# releaseName: yugabytedb +# namespace: db +# # This replaces the "--set" flags you used in the command line +# valuesInline: +# storage: +# master: +# storageClass: longhorn +# tserver: +# storageClass: longhorn +# replicas: +# master: 1 +# tserver: 3 +# enableLoadBalancer: false +# gflags: +# master: +# max_clock_skew_usec: 2000000 +# time_source: system +# tserver: +# max_clock_skew_usec: 2000000 +# time_source: system +# start_pgsql_proxy: true \ No newline at end of file diff --git a/yugabyte/yugabytedb.yaml b/yugabyte/yugabytedb.yaml new file mode 100644 index 0000000000000000000000000000000000000000..fc4aa549a3e5241dff3a167bacd641b5129b46ad GIT binary patch literal 52350 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 Kt$JVLT>b+;$3vq4 literal 0 HcmV?d00001 diff --git a/yugabyte/yugabytedb2.yaml b/yugabyte/yugabytedb2.yaml new file mode 100644 index 0000000000000000000000000000000000000000..86f92888ab459e42548905fdc979b3a1133cf361 GIT binary patch literal 51418 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 literal 0 HcmV?d00001