diff --git a/manifests/charts/istio-control/istio-discovery/files/kube-gateway.yaml b/manifests/charts/istio-control/istio-discovery/files/kube-gateway.yaml index dbe158de1f..aa2dcff031 100644 --- a/manifests/charts/istio-control/istio-discovery/files/kube-gateway.yaml +++ b/manifests/charts/istio-control/istio-discovery/files/kube-gateway.yaml @@ -343,3 +343,51 @@ spec: {{- end }} type: {{ .ServiceType | quote }} --- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{.DeploymentName | quote}} + namespace: {{.Namespace | quote}} + annotations: + {{- toJsonMap (omit .InfrastructureAnnotations "kubectl.kubernetes.io/last-applied-configuration" "gateway.istio.io/name-override" "gateway.istio.io/service-account" "gateway.istio.io/controller-version") | nindent 4 }} + labels: + {{- toJsonMap + .InfrastructureLabels + (strdict + "gateway.networking.k8s.io/gateway-name" .Name + ) | nindent 4 }} + ownerReferences: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + name: {{.Name}} + uid: "{{.UID}}" +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{.DeploymentName | quote}} + maxReplicas: 1 +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{.DeploymentName | quote}} + namespace: {{.Namespace | quote}} + annotations: + {{- toJsonMap (omit .InfrastructureAnnotations "kubectl.kubernetes.io/last-applied-configuration" "gateway.istio.io/name-override" "gateway.istio.io/service-account" "gateway.istio.io/controller-version") | nindent 4 }} + labels: + {{- toJsonMap + .InfrastructureLabels + (strdict + "gateway.networking.k8s.io/gateway-name" .Name + ) | nindent 4 }} + ownerReferences: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + name: {{.Name}} + uid: "{{.UID}}" +spec: + selector: + matchLabels: + gateway.networking.k8s.io/gateway-name: {{.Name|quote}} + diff --git a/manifests/charts/istio-control/istio-discovery/files/waypoint.yaml b/manifests/charts/istio-control/istio-discovery/files/waypoint.yaml index 2600e98e21..87b359c947 100644 --- a/manifests/charts/istio-control/istio-discovery/files/waypoint.yaml +++ b/manifests/charts/istio-control/istio-discovery/files/waypoint.yaml @@ -338,3 +338,51 @@ spec: {{- end }} type: {{ .ServiceType | quote }} --- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{.DeploymentName | quote}} + namespace: {{.Namespace | quote}} + annotations: + {{- toJsonMap (omit .InfrastructureAnnotations "kubectl.kubernetes.io/last-applied-configuration" "gateway.istio.io/name-override" "gateway.istio.io/service-account" "gateway.istio.io/controller-version") | nindent 4 }} + labels: + {{- toJsonMap + .InfrastructureLabels + (strdict + "gateway.networking.k8s.io/gateway-name" .Name + ) | nindent 4 }} + ownerReferences: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + name: {{.Name}} + uid: "{{.UID}}" +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{.DeploymentName | quote}} + maxReplicas: 1 +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{.DeploymentName | quote}} + namespace: {{.Namespace | quote}} + annotations: + {{- toJsonMap (omit .InfrastructureAnnotations "kubectl.kubernetes.io/last-applied-configuration" "gateway.istio.io/name-override" "gateway.istio.io/service-account" "gateway.istio.io/controller-version") | nindent 4 }} + labels: + {{- toJsonMap + .InfrastructureLabels + (strdict + "gateway.networking.k8s.io/gateway-name" .Name + ) | nindent 4 }} + ownerReferences: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + name: {{.Name}} + uid: "{{.UID}}" +spec: + selector: + matchLabels: + gateway.networking.k8s.io/gateway-name: {{.Name|quote}} + diff --git a/manifests/charts/istio-control/istio-discovery/templates/clusterrole.yaml b/manifests/charts/istio-control/istio-discovery/templates/clusterrole.yaml index 0c340b5b3b..30c75fb75b 100644 --- a/manifests/charts/istio-control/istio-discovery/templates/clusterrole.yaml +++ b/manifests/charts/istio-control/istio-discovery/templates/clusterrole.yaml @@ -177,6 +177,12 @@ rules: - apiGroups: ["apps"] verbs: [ "get", "watch", "list", "update", "patch", "create", "delete" ] resources: [ "deployments" ] + - apiGroups: ["autoscaling"] + verbs: [ "get", "watch", "list", "update", "patch", "create", "delete" ] + resources: [ "horizontalpodautoscalers" ] + - apiGroups: ["policy"] + verbs: [ "get", "watch", "list", "update", "patch", "create", "delete" ] + resources: [ "poddisruptionbudgets" ] - apiGroups: [""] verbs: [ "get", "watch", "list", "update", "patch", "create", "delete" ] resources: [ "services" ] diff --git a/manifests/charts/istio-control/istio-discovery/templates/gateway-class-configmap.yaml b/manifests/charts/istio-control/istio-discovery/templates/gateway-class-configmap.yaml new file mode 100644 index 0000000000..06e343f156 --- /dev/null +++ b/manifests/charts/istio-control/istio-discovery/templates/gateway-class-configmap.yaml @@ -0,0 +1,21 @@ +{{ range $key, $value := .Values.gatewayClasses }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: istio-{{ $.Values.revision | default "default" }}-gatewayclass-{{$key}} + namespace: {{ $.Release.Namespace }} + labels: + istio.io/rev: {{ $.Values.revision | default "default" | quote }} + install.operator.istio.io/owning-resource: {{ $.Values.ownerName | default "unknown" }} + operator.istio.io/component: "Pilot" + release: {{ $.Release.Name }} + app.kubernetes.io/name: "istiod" + gateway.istio.io/defaults-for-class: {{$key|quote}} + {{- include "istio.labels" $ | nindent 4 }} +data: +{{ range $kind, $overlay := $value }} + {{$kind}}: | +{{$overlay|toYaml|trim|indent 4}} +{{ end }} +--- +{{ end }} diff --git a/manifests/charts/istio-control/istio-discovery/values.yaml b/manifests/charts/istio-control/istio-discovery/values.yaml index 5f17db0753..1c329e7750 100644 --- a/manifests/charts/istio-control/istio-discovery/values.yaml +++ b/manifests/charts/istio-control/istio-discovery/values.yaml @@ -539,3 +539,13 @@ _internal_defaults_do_not_set: # Set to `type: RuntimeDefault` to use the default profile for templated gateways, if your container runtime supports it seccompProfile: {} + + # gatewayClasses allows customizing the configuration of the default deployment of Gateways per GatewayClass. + # For example: + # gatewayClasses: + # istio: + # service: + # spec: + # type: ClusterIP + # Per-Gateway configuration can also be set in the `Gateway.spec.infrastructure.parametersRef` field. + gatewayClasses: {} diff --git a/operator/pkg/apis/values_types.pb.go b/operator/pkg/apis/values_types.pb.go index 5ddceb7b17..9a3c3f9fe1 100644 --- a/operator/pkg/apis/values_types.pb.go +++ b/operator/pkg/apis/values_types.pb.go @@ -4964,9 +4964,11 @@ type Values struct { // be configured with the same defaults as the specified version. CompatibilityVersion string `protobuf:"bytes,43,opt,name=compatibilityVersion,proto3" json:"compatibilityVersion,omitempty"` // Specifies experimental helm fields that could be removed or changed in the future - Experimental *ExperimentalConfig `protobuf:"bytes,44,opt,name=experimental,proto3" json:"experimental,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Experimental *ExperimentalConfig `protobuf:"bytes,44,opt,name=experimental,proto3" json:"experimental,omitempty"` + // Configuration for Gateway Classes + GatewayClasses *structpb.Value `protobuf:"bytes,45,opt,name=gatewayClasses,proto3" json:"gatewayClasses,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Values) Reset() { @@ -5127,6 +5129,13 @@ func (x *Values) GetExperimental() *ExperimentalConfig { return nil } +func (x *Values) GetGatewayClasses() *structpb.Value { + if x != nil { + return x.GatewayClasses + } + return nil +} + // ZeroVPNConfig enables cross-cluster access using SNI matching. type ZeroVPNConfig struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -6518,7 +6527,7 @@ var file_pkg_apis_values_types_proto_rawDesc = string([]byte{ 0x34, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x07, 0x65, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x97, 0x08, 0x0a, 0x06, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xd7, 0x08, 0x0a, 0x06, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x03, 0x63, 0x6e, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x69, 0x73, 0x74, 0x69, 0x6f, 0x2e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x43, 0x4e, 0x49, 0x43, 0x6f, 0x6e, 0x66, 0x69, @@ -6583,7 +6592,11 @@ var file_pkg_apis_values_types_proto_rawDesc = string([]byte{ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x69, 0x73, 0x74, 0x69, 0x6f, 0x2e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x52, 0x0c, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x22, + 0x67, 0x52, 0x0c, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x12, + 0x3e, 0x0a, 0x0e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x65, + 0x73, 0x18, 0x2d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x0e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x65, 0x73, 0x22, 0x5d, 0x0a, 0x0d, 0x5a, 0x65, 0x72, 0x6f, 0x56, 0x50, 0x4e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, @@ -6914,20 +6927,21 @@ var file_pkg_apis_values_types_proto_depIdxs = []int32{ 45, // 177: istio.operator.v1alpha1.Values.base:type_name -> istio.operator.v1alpha1.BaseConfig 46, // 178: istio.operator.v1alpha1.Values.istiodRemote:type_name -> istio.operator.v1alpha1.IstiodRemoteConfig 49, // 179: istio.operator.v1alpha1.Values.experimental:type_name -> istio.operator.v1alpha1.ExperimentalConfig - 56, // 180: istio.operator.v1alpha1.ZeroVPNConfig.enabled:type_name -> google.protobuf.BoolValue - 56, // 181: istio.operator.v1alpha1.ExperimentalConfig.stableValidationPolicy:type_name -> google.protobuf.BoolValue - 68, // 182: istio.operator.v1alpha1.IntOrString.intVal:type_name -> google.protobuf.Int32Value - 69, // 183: istio.operator.v1alpha1.IntOrString.strVal:type_name -> google.protobuf.StringValue - 10, // 184: istio.operator.v1alpha1.WaypointConfig.resources:type_name -> istio.operator.v1alpha1.Resources - 58, // 185: istio.operator.v1alpha1.WaypointConfig.affinity:type_name -> k8s.io.api.core.v1.Affinity - 64, // 186: istio.operator.v1alpha1.WaypointConfig.topologySpreadConstraints:type_name -> k8s.io.api.core.v1.TopologySpreadConstraint - 70, // 187: istio.operator.v1alpha1.WaypointConfig.nodeSelector:type_name -> k8s.io.api.core.v1.NodeSelector - 62, // 188: istio.operator.v1alpha1.WaypointConfig.toleration:type_name -> k8s.io.api.core.v1.Toleration - 189, // [189:189] is the sub-list for method output_type - 189, // [189:189] is the sub-list for method input_type - 189, // [189:189] is the sub-list for extension type_name - 189, // [189:189] is the sub-list for extension extendee - 0, // [0:189] is the sub-list for field type_name + 57, // 180: istio.operator.v1alpha1.Values.gatewayClasses:type_name -> google.protobuf.Value + 56, // 181: istio.operator.v1alpha1.ZeroVPNConfig.enabled:type_name -> google.protobuf.BoolValue + 56, // 182: istio.operator.v1alpha1.ExperimentalConfig.stableValidationPolicy:type_name -> google.protobuf.BoolValue + 68, // 183: istio.operator.v1alpha1.IntOrString.intVal:type_name -> google.protobuf.Int32Value + 69, // 184: istio.operator.v1alpha1.IntOrString.strVal:type_name -> google.protobuf.StringValue + 10, // 185: istio.operator.v1alpha1.WaypointConfig.resources:type_name -> istio.operator.v1alpha1.Resources + 58, // 186: istio.operator.v1alpha1.WaypointConfig.affinity:type_name -> k8s.io.api.core.v1.Affinity + 64, // 187: istio.operator.v1alpha1.WaypointConfig.topologySpreadConstraints:type_name -> k8s.io.api.core.v1.TopologySpreadConstraint + 70, // 188: istio.operator.v1alpha1.WaypointConfig.nodeSelector:type_name -> k8s.io.api.core.v1.NodeSelector + 62, // 189: istio.operator.v1alpha1.WaypointConfig.toleration:type_name -> k8s.io.api.core.v1.Toleration + 190, // [190:190] is the sub-list for method output_type + 190, // [190:190] is the sub-list for method input_type + 190, // [190:190] is the sub-list for extension type_name + 190, // [190:190] is the sub-list for extension extendee + 0, // [0:190] is the sub-list for field type_name } func init() { file_pkg_apis_values_types_proto_init() } diff --git a/operator/pkg/apis/values_types.proto b/operator/pkg/apis/values_types.proto index 14f83781a1..85a77b2392 100644 --- a/operator/pkg/apis/values_types.proto +++ b/operator/pkg/apis/values_types.proto @@ -1412,6 +1412,9 @@ message Values { // Specifies experimental helm fields that could be removed or changed in the future ExperimentalConfig experimental = 44; + + // Configuration for Gateway Classes + google.protobuf.Value gatewayClasses = 45; } // ZeroVPNConfig enables cross-cluster access using SNI matching. diff --git a/pilot/pkg/bootstrap/configcontroller.go b/pilot/pkg/bootstrap/configcontroller.go index f4a4989ef1..907bde2291 100644 --- a/pilot/pkg/bootstrap/configcontroller.go +++ b/pilot/pkg/bootstrap/configcontroller.go @@ -193,7 +193,7 @@ func (s *Server) initK8SConfigStore(args *PilotArgs) error { if s.kubeClient.CrdWatcher().WaitForCRD(gvr.KubernetesGateway, leaderStop) { tagWatcher := revisions.NewTagWatcher(s.kubeClient, args.Revision) controller := gateway.NewDeploymentController(s.kubeClient, s.clusterID, s.environment, - s.webhookInfo.getWebhookConfig, s.webhookInfo.addHandler, tagWatcher, args.Revision) + s.webhookInfo.getWebhookConfig, s.webhookInfo.addHandler, tagWatcher, args.Revision, args.Namespace) // Start informers again. This fixes the case where informers for namespace do not start, // as we create them only after acquiring the leader lock // Note: stop here should be the overall pilot stop, NOT the leader election stop. We are diff --git a/pilot/pkg/bootstrap/server.go b/pilot/pkg/bootstrap/server.go index 98ca2bf91b..cfb0feb300 100644 --- a/pilot/pkg/bootstrap/server.go +++ b/pilot/pkg/bootstrap/server.go @@ -153,7 +153,8 @@ type Server struct { RA ra.RegistrationAuthority caServer *caserver.Server - // TrustAnchors for workload to workload mTLS + // TrustAnchors for workload to workload mTLS and proxy to istiod TLS + // Only initiated when `ISTIO_MULTIROOT_MESH` = true workloadTrustBundle *tb.TrustBundle certMu sync.RWMutex istiodCert *tls.Certificate @@ -298,9 +299,11 @@ func NewServer(args *PilotArgs, initFuncs ...func(*Server)) (*Server, error) { return nil, err } - // Initialize trust bundle after mesh config which it depends on - s.workloadTrustBundle = tb.NewTrustBundle(nil, e.Watcher) - e.TrustBundle = s.workloadTrustBundle + if features.MultiRootMesh { + // Initialize trust bundle after mesh config which it depends on + s.workloadTrustBundle = tb.NewTrustBundle(nil, e.Watcher) + e.TrustBundle = s.workloadTrustBundle + } // Options based on the current 'defaults' in istio. caOpts := &caOptions{ diff --git a/pilot/pkg/config/kube/crdclient/types.gen.go b/pilot/pkg/config/kube/crdclient/types.gen.go index a77e1cfc71..91ef06a13d 100644 --- a/pilot/pkg/config/kube/crdclient/types.gen.go +++ b/pilot/pkg/config/kube/crdclient/types.gen.go @@ -16,11 +16,13 @@ import ( k8sioapiadmissionregistrationv1 "k8s.io/api/admissionregistration/v1" k8sioapiappsv1 "k8s.io/api/apps/v1" + k8sioapiautoscalingv2 "k8s.io/api/autoscaling/v2" k8sioapicertificatesv1 "k8s.io/api/certificates/v1" k8sioapicoordinationv1 "k8s.io/api/coordination/v1" k8sioapicorev1 "k8s.io/api/core/v1" k8sioapidiscoveryv1 "k8s.io/api/discovery/v1" k8sioapinetworkingv1 "k8s.io/api/networking/v1" + k8sioapipolicyv1 "k8s.io/api/policy/v1" k8sioapiextensionsapiserverpkgapisapiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" sigsk8siogatewayapiapisv1 "sigs.k8s.io/gateway-api/apis/v1" sigsk8siogatewayapiapisv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" @@ -1042,6 +1044,25 @@ var translationMap = map[config.GroupVersionKind]func(r runtime.Object) config.C Status: &obj.Status, } }, + gvk.HorizontalPodAutoscaler: func(r runtime.Object) config.Config { + obj := r.(*k8sioapiautoscalingv2.HorizontalPodAutoscaler) + return config.Config{ + Meta: config.Meta{ + GroupVersionKind: gvk.HorizontalPodAutoscaler, + Name: obj.Name, + Namespace: obj.Namespace, + Labels: obj.Labels, + Annotations: obj.Annotations, + ResourceVersion: obj.ResourceVersion, + CreationTimestamp: obj.CreationTimestamp.Time, + OwnerReferences: obj.OwnerReferences, + UID: string(obj.UID), + Generation: obj.Generation, + }, + Spec: &obj.Spec, + Status: &obj.Status, + } + }, gvk.Ingress: func(r runtime.Object) config.Config { obj := r.(*k8sioapinetworkingv1.Ingress) return config.Config{ @@ -1207,6 +1228,25 @@ var translationMap = map[config.GroupVersionKind]func(r runtime.Object) config.C Spec: &obj.Spec, } }, + gvk.PodDisruptionBudget: func(r runtime.Object) config.Config { + obj := r.(*k8sioapipolicyv1.PodDisruptionBudget) + return config.Config{ + Meta: config.Meta{ + GroupVersionKind: gvk.PodDisruptionBudget, + Name: obj.Name, + Namespace: obj.Namespace, + Labels: obj.Labels, + Annotations: obj.Annotations, + ResourceVersion: obj.ResourceVersion, + CreationTimestamp: obj.CreationTimestamp.Time, + OwnerReferences: obj.OwnerReferences, + UID: string(obj.UID), + Generation: obj.Generation, + }, + Spec: &obj.Spec, + Status: &obj.Status, + } + }, gvk.ProxyConfig: func(r runtime.Object) config.Config { obj := r.(*apiistioioapinetworkingv1beta1.ProxyConfig) return config.Config{ diff --git a/pilot/pkg/config/kube/gateway/deploymentcontroller.go b/pilot/pkg/config/kube/gateway/deploymentcontroller.go index 71044bee6d..60a9ab7568 100644 --- a/pilot/pkg/config/kube/gateway/deploymentcontroller.go +++ b/pilot/pkg/config/kube/gateway/deploymentcontroller.go @@ -18,16 +18,21 @@ import ( "context" "encoding/json" "fmt" + "reflect" "strconv" "strings" appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" klabels "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/mergepatch" + "k8s.io/apimachinery/pkg/util/strategicpatch" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" gateway "sigs.k8s.io/gateway-api/apis/v1beta1" "sigs.k8s.io/yaml" @@ -35,9 +40,11 @@ import ( "istio.io/api/annotation" "istio.io/api/label" meshapi "istio.io/api/mesh/v1alpha1" + "istio.io/istio/pilot/pkg/config/kube/crd" "istio.io/istio/pilot/pkg/features" "istio.io/istio/pilot/pkg/model" "istio.io/istio/pkg/cluster" + "istio.io/istio/pkg/config" "istio.io/istio/pkg/config/constants" "istio.io/istio/pkg/config/protocol" "istio.io/istio/pkg/config/schema/gvk" @@ -50,6 +57,7 @@ import ( istiolog "istio.io/istio/pkg/log" "istio.io/istio/pkg/maps" "istio.io/istio/pkg/revisions" + "istio.io/istio/pkg/slices" "istio.io/istio/pkg/test/util/tmpl" "istio.io/istio/pkg/test/util/yml" "istio.io/istio/pkg/util/sets" @@ -91,10 +99,14 @@ type DeploymentController struct { injectConfig func() inject.WebhookConfig deployments kclient.Client[*appsv1.Deployment] services kclient.Client[*corev1.Service] + hpas kclient.Client[*autoscalingv2.HorizontalPodAutoscaler] + pdbs kclient.Client[*policyv1.PodDisruptionBudget] + configMaps kclient.Client[*corev1.ConfigMap] serviceAccounts kclient.Client[*corev1.ServiceAccount] namespaces kclient.Client[*corev1.Namespace] tagWatcher revisions.TagWatcher revision string + systemNamespace string } // Patcher is a function that abstracts patching logic. This is largely because client-go fakes do not handle patching @@ -179,8 +191,15 @@ func getClassInfos() map[gateway.GatewayController]classInfo { // NewDeploymentController constructs a DeploymentController and registers required informers. // The controller will not start until Run() is called. -func NewDeploymentController(client kube.Client, clusterID cluster.ID, env *model.Environment, - webhookConfig func() inject.WebhookConfig, injectionHandler func(fn func()), tw revisions.TagWatcher, revision string, +func NewDeploymentController( + client kube.Client, + clusterID cluster.ID, + env *model.Environment, + webhookConfig func() inject.WebhookConfig, + injectionHandler func(fn func()), + tw revisions.TagWatcher, + revision string, + systemNamespace string, ) *DeploymentController { filter := kclient.Filter{ObjectFilter: client.ObjectFilter()} gateways := kclient.NewFiltered[*gateway.Gateway](client, filter) @@ -199,12 +218,20 @@ func NewDeploymentController(client kube.Client, clusterID cluster.ID, env *mode }, subresources...) return err }, - gateways: gateways, - gatewayClasses: gatewayClasses, - injectConfig: webhookConfig, - tagWatcher: tw, - revision: revision, - } + gateways: gateways, + gatewayClasses: gatewayClasses, + injectConfig: webhookConfig, + tagWatcher: tw, + revision: revision, + systemNamespace: systemNamespace, + } + gatewaysByParamsRef := kclient.CreateIndex(gateways, func(o *gateway.Gateway) []types.NamespacedName { + p, err := fetchParameters(o) + if p == nil || err != nil { + return nil + } + return []types.NamespacedName{*p} + }) dc.queue = controllers.NewQueue("gateway deployment", controllers.WithReconciler(dc.Reconcile), controllers.WithMaxAttempts(5)) @@ -218,6 +245,24 @@ func NewDeploymentController(client kube.Client, clusterID cluster.ID, env *mode dc.services.AddEventHandler(parentHandler) dc.clients[gvr.Service] = NewUntypedWrapper(dc.services) + dc.configMaps = kclient.NewFiltered[*corev1.ConfigMap](client, filter) + dc.configMaps.AddEventHandler(controllers.ObjectHandler(func(o controllers.Object) { + // This could be a configmap referenced by a Gateway paramsRef + impacted := gatewaysByParamsRef.Lookup(config.NamespacedName(o)) + for _, gw := range impacted { + dc.queue.AddObject(gw) + } + // Or it could also be a global GatewayClass config + classDefaults, classDefaultsF := o.GetLabels()[gatewayClassDefaults] + if classDefaultsF && o.GetNamespace() == dc.systemNamespace { + for _, gw := range dc.gateways.List(metav1.NamespaceAll, klabels.Everything()) { + if string(gw.Spec.GatewayClassName) == classDefaults { + dc.queue.AddObject(gw) + } + } + } + })) + dc.deployments = kclient.NewFiltered[*appsv1.Deployment](client, filter) dc.deployments.AddEventHandler(parentHandler) dc.clients[gvr.Deployment] = NewUntypedWrapper(dc.deployments) @@ -226,6 +271,14 @@ func NewDeploymentController(client kube.Client, clusterID cluster.ID, env *mode dc.serviceAccounts.AddEventHandler(parentHandler) dc.clients[gvr.ServiceAccount] = NewUntypedWrapper(dc.serviceAccounts) + dc.hpas = kclient.NewFiltered[*autoscalingv2.HorizontalPodAutoscaler](client, filter) + dc.hpas.AddEventHandler(parentHandler) + dc.clients[gvr.HorizontalPodAutoscaler] = NewUntypedWrapper(dc.hpas) + + dc.pdbs = kclient.NewFiltered[*policyv1.PodDisruptionBudget](client, filter) + dc.pdbs.AddEventHandler(parentHandler) + dc.clients[gvr.PodDisruptionBudget] = NewUntypedWrapper(dc.pdbs) + dc.namespaces = kclient.NewFiltered[*corev1.Namespace](client, filter) dc.namespaces.AddEventHandler(controllers.ObjectHandler(func(o controllers.Object) { // TODO: make this more intelligent, checking if something we care about has changed @@ -263,13 +316,26 @@ func (d *DeploymentController) Run(stop <-chan struct{}) { d.namespaces.HasSynced, d.deployments.HasSynced, d.services.HasSynced, + d.configMaps.HasSynced, d.serviceAccounts.HasSynced, + d.hpas.HasSynced, + d.pdbs.HasSynced, d.gateways.HasSynced, d.gatewayClasses.HasSynced, d.tagWatcher.HasSynced, ) d.queue.Run(stop) - controllers.ShutdownAll(d.namespaces, d.deployments, d.services, d.serviceAccounts, d.gateways, d.gatewayClasses) + controllers.ShutdownAll( + d.namespaces, + d.deployments, + d.services, + d.configMaps, + d.serviceAccounts, + d.hpas, + d.pdbs, + d.gateways, + d.gatewayClasses, + ) } // Reconcile takes in the name of a Gateway and ensures the cluster is in the desired state @@ -374,7 +440,9 @@ func (d *DeploymentController) configureIstioGateway(log *istiolog.Scope, gw gat rendered, err := d.render(gi.templates, input) if err != nil { - return fmt.Errorf("failed to render template: %v", err) + // Just log error, we do not need to retry since rendering errors are not ephemeral errors + log.Errorf("error rendering templates: %v", err) + return nil } for _, t := range rendered { if err := d.apply(gi.controller, t); err != nil { @@ -514,6 +582,27 @@ func (d *DeploymentController) render(templateName string, mi TemplateInput) ([] return nil, fmt.Errorf("no %q template defined", templateName) } + var templateOverlays []map[string]string + + classConfigs := d.configMaps.List(d.systemNamespace, klabels.SelectorFromValidatedSet(map[string]string{ + gatewayClassDefaults: string(mi.Spec.GatewayClassName), + })) + if len(classConfigs) > 0 { + classConfig := controllers.OldestObject(classConfigs) + templateOverlays = append(templateOverlays, classConfig.Data) + } + params, err := fetchParameters(mi.Gateway) + if err != nil { + return nil, fmt.Errorf("invalid parameters: %v", err) + } + if params != nil { + cm := d.configMaps.Get(params.Name, params.Namespace) + if cm == nil { + return nil, fmt.Errorf("parametersRef targeting configmap %q, but configmap does not exist", params) + } + templateOverlays = append(templateOverlays, cm.Data) + } + labelToMatch := map[string]string{label.IoK8sNetworkingGatewayGatewayName.Name: mi.Name} proxyConfig := d.env.GetProxyConfigOrDefault(mi.Namespace, labelToMatch, nil, cfg.MeshConfig) input := derivedInput{ @@ -532,7 +621,129 @@ func (d *DeploymentController) render(templateName string, mi TemplateInput) ([] return nil, err } - return yml.SplitString(results), nil + rawOutput := yml.SplitString(results) + transformedOutput := make([]string, 0, len(rawOutput)) + for _, output := range rawOutput { + to, err := applyOverlay(output, templateOverlays) + if err != nil { + return nil, err + } + if to != "" { + transformedOutput = append(transformedOutput, to) + } + } + return transformedOutput, nil +} + +var supportedOverlays = sets.New( + "deployment", + "service", + "serviceAccount", + "horizontalPodAutoscaler", + "podDisruptionBudget", +) + +var requiredOverlays = sets.New( + "horizontalPodAutoscaler", + "podDisruptionBudget", +) + +func applyOverlay(object string, overlaysList []map[string]string) (string, error) { + var ik crd.IstioKind + if err := yaml.Unmarshal([]byte(object), &ik); err != nil { + return "", fmt.Errorf("failed to find kind: %v", err) + } + gv, err := schema.ParseGroupVersion(ik.TypeMeta.APIVersion) + if err != nil { + return "", fmt.Errorf("failed to find kind: %v", err) + } + kind := &schema.GroupVersionKind{Group: gv.Group, Version: gv.Version, Kind: ik.TypeMeta.Kind} + + var data any + var key string + switch kind.Kind { + case gvk.Deployment.Kind: + data = &appsv1.Deployment{} + key = "deployment" + case gvk.Service.Kind: + data = &corev1.Service{} + key = "service" + case gvk.ServiceAccount.Kind: + data = &corev1.ServiceAccount{} + key = "serviceAccount" + case gvk.HorizontalPodAutoscaler.Kind: + data = &autoscalingv2.HorizontalPodAutoscaler{} + key = "horizontalPodAutoscaler" + case gvk.PodDisruptionBudget.Kind: + data = &policyv1.PodDisruptionBudget{} + key = "podDisruptionBudget" + default: + return "", fmt.Errorf("unknown overlay kind %q", kind.Kind) + } + applied := false + for _, overlays := range overlaysList { + for k := range overlays { + if !supportedOverlays.Contains(k) { + return "", fmt.Errorf("unsupported overlay %q (supported: %v)", k, sets.SortedList(supportedOverlays)) + } + } + overlay, f := overlays[key] + if !f { + continue + } + b, err := strategicMergePatchYAML([]byte(object), []byte(overlay), data) + if err != nil { + return "", fmt.Errorf("strategic merge patch failed: %v", err) + } + applied = true + object = string(b) + } + if !applied && requiredOverlays.Contains(key) { + return "", nil + } + + var finalIK crd.IstioKind + if err := yaml.Unmarshal([]byte(object), &finalIK); err != nil { + return "", fmt.Errorf("failed to find final kind: %v", err) + } + + a, b := ik.ObjectMeta, finalIK.ObjectMeta + if !(a.Name == b.Name && + a.GenerateName == b.GenerateName && + a.Namespace == b.Namespace && + a.UID == b.UID && + a.ResourceVersion == b.ResourceVersion && + a.Generation == b.Generation && + a.CreationTimestamp == b.CreationTimestamp && + a.DeletionTimestamp == b.DeletionTimestamp && + a.DeletionGracePeriodSeconds == b.DeletionGracePeriodSeconds && + reflect.DeepEqual(a.OwnerReferences, b.OwnerReferences) && + slices.Equal(a.Finalizers, b.Finalizers)) { + return "", fmt.Errorf("illegal metadata change") + } + // We could deep equal here but its a bit more tedious, so just never allow setting it + if len(a.ManagedFields) != 0 || len(b.ManagedFields) != 0 { + return "", fmt.Errorf("illegal metadata change") + } + + return object, nil +} + +// fetchParameters returns the infrastructure parameters for the Gateway. This is currently always a local configmap so we return the name only. +// An error is returned if the parameter is invalid. This does not check the configmap exists, though. +// If no parameter is specified, no name or error is returned. +func fetchParameters(gw *gateway.Gateway) (*types.NamespacedName, error) { + if gw.Spec.Infrastructure != nil && gw.Spec.Infrastructure.ParametersRef != nil { + pr := gw.Spec.Infrastructure.ParametersRef + if string(pr.Kind) == gvk.ConfigMap.Kind && string(pr.Group) == gvk.ConfigMap.Group { + return &types.NamespacedName{ + Namespace: gw.Namespace, + Name: pr.Name, + }, nil + } + return nil, fmt.Errorf("unknown infrastructure parameters type %v/%v", pr.Group, pr.Kind) + } + return nil, nil } func (d *DeploymentController) setGatewayControllerVersion(gws gateway.Gateway) error { @@ -701,3 +912,41 @@ func (u UntypedWrapper[T]) Get(name, namespace string) controllers.Object { } var _ getter = UntypedWrapper[*corev1.Service]{} + +// strategicMergePatchYAML is a small fork of strategicpatch.StrategicMergePatch to allow YAML patches +// This avoids expensive conversion from YAML to JSON +func strategicMergePatchYAML(originalYAML []byte, patchYAML []byte, dataStruct any) ([]byte, error) { + schema, err := strategicpatch.NewPatchMetaFromStruct(dataStruct) + if err != nil { + return nil, err + } + + originalMap, err := patchHandleUnmarshal(originalYAML) + if err != nil { + return nil, err + } + patchMap, err := patchHandleUnmarshal(patchYAML) + if err != nil { + return nil, err + } + + result, err := strategicpatch.StrategicMergeMapPatchUsingLookupPatchMeta(originalMap, patchMap, schema) + if err != nil { + return nil, err + } + + return json.Marshal(result) +} + +func patchHandleUnmarshal(j []byte) (map[string]any, error) { + if j == nil { + j = []byte("{}") + } + + m := map[string]any{} + err := yaml.Unmarshal(j, &m) + if err != nil { + return nil, mergepatch.ErrBadJSONDoc + } + return m, nil +} diff --git a/pilot/pkg/config/kube/gateway/deploymentcontroller_test.go b/pilot/pkg/config/kube/gateway/deploymentcontroller_test.go index 3054785e6f..c9bd6a4f33 100644 --- a/pilot/pkg/config/kube/gateway/deploymentcontroller_test.go +++ b/pilot/pkg/config/kube/gateway/deploymentcontroller_test.go @@ -55,6 +55,7 @@ import ( "istio.io/istio/pkg/kube/kubetypes" istiolog "istio.io/istio/pkg/log" "istio.io/istio/pkg/revisions" + "istio.io/istio/pkg/slices" "istio.io/istio/pkg/test" "istio.io/istio/pkg/test/env" "istio.io/istio/pkg/test/util/assert" @@ -396,6 +397,81 @@ func TestConfigureIstioGateway(t *testing.T) { tag: test network: network-1`, }, + { + name: "customizations", + gw: k8sbeta.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "namespace", + Namespace: "default", + }, + Spec: k8s.GatewaySpec{ + GatewayClassName: k8s.ObjectName(features.GatewayAPIDefaultGatewayClass), + Infrastructure: &k8s.GatewayInfrastructure{ + Labels: map[k8s.LabelKey]k8s.LabelValue{"foo": "bar"}, + ParametersRef: &k8s.LocalParametersReference{ + Group: "", + Kind: "ConfigMap", + Name: "gw-options", + }, + }, + }, + }, + objects: append(slices.Clone(defaultObjects), &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "gw-options", Namespace: "default"}, + Data: map[string]string{ + "podDisruptionBudget": ` +spec: + minAvailable: 1`, + "horizontalPodAutoscaler": ` +spec: + minReplicas: 2 + maxReplicas: 2`, + "deployment": ` +metadata: + annotations: + cm-annotation: cm-annotation-value +spec: + replicas: 4 + template: + spec: + containers: + - name: istio-proxy + resources: + requests: + cpu: 222m`, + }, + }), + values: ``, + }, + { + name: "illegal_customizations", + gw: k8sbeta.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "namespace", + Namespace: "default", + }, + Spec: k8s.GatewaySpec{ + GatewayClassName: k8s.ObjectName(features.GatewayAPIDefaultGatewayClass), + Infrastructure: &k8s.GatewayInfrastructure{ + Labels: map[k8s.LabelKey]k8s.LabelValue{"foo": "bar"}, + ParametersRef: &k8s.LocalParametersReference{ + Group: "", + Kind: "ConfigMap", + Name: "gw-options", + }, + }, + }, + }, + objects: append(slices.Clone(defaultObjects), &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "gw-options", Namespace: "default"}, + Data: map[string]string{ + "deployment": ` +metadata: + name: not-allowed`, + }, + }), + values: ``, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -411,9 +487,8 @@ func TestConfigureIstioGateway(t *testing.T) { env.PushContext().ProxyConfigs = tt.pcs tw := revisions.NewTagWatcher(client, "") go tw.Run(stop) - d := NewDeploymentController( - client, cluster.ID(features.ClusterName), env, testInjectionConfig(t, tt.values), func(fn func()) { - }, tw, "") + d := NewDeploymentController(client, cluster.ID(features.ClusterName), env, testInjectionConfig(t, tt.values), func(fn func()) { + }, tw, "", "") d.patcher = func(gvr schema.GroupVersionResource, name string, namespace string, data []byte, subresources ...string) error { b, err := yaml.JSONToYAML(data) if err != nil { @@ -473,7 +548,7 @@ func TestVersionManagement(t *testing.T) { }) tw := revisions.NewTagWatcher(c, "default") env := &model.Environment{} - d := NewDeploymentController(c, "", env, testInjectionConfig(t, ""), func(fn func()) {}, tw, "") + d := NewDeploymentController(c, "", env, testInjectionConfig(t, ""), func(fn func()) {}, tw, "", "") reconciles := atomic.NewInt32(0) wantReconcile := int32(0) expectReconciled := func() { diff --git a/pilot/pkg/config/kube/gateway/model.go b/pilot/pkg/config/kube/gateway/model.go index f4c6272261..71c51dba0f 100644 --- a/pilot/pkg/config/kube/gateway/model.go +++ b/pilot/pkg/config/kube/gateway/model.go @@ -29,6 +29,7 @@ import ( const ( gatewayTLSTerminateModeKey = "gateway.istio.io/tls-terminate-mode" addressTypeOverride = "networking.istio.io/address-type" + gatewayClassDefaults = "gateway.istio.io/defaults-for-class" ) // GatewayResources stores all gateway resources used for our conversion. diff --git a/pilot/pkg/config/kube/gateway/testdata/deployment/customizations.yaml b/pilot/pkg/config/kube/gateway/testdata/deployment/customizations.yaml new file mode 100644 index 0000000000..d643f1a3ed --- /dev/null +++ b/pilot/pkg/config/kube/gateway/testdata/deployment/customizations.yaml @@ -0,0 +1,300 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: Gateway +metadata: + annotations: + gateway.istio.io/controller-version: "5" +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: {} + labels: + foo: bar + gateway.istio.io/managed: istio.io-gateway-controller + gateway.networking.k8s.io/gateway-name: namespace + istio.io/dataplane-mode: none + name: namespace-istio + namespace: default + ownerReferences: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + name: namespace + uid: "" +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + cm-annotation: cm-annotation-value + labels: + foo: bar + gateway.istio.io/managed: istio.io-gateway-controller + gateway.networking.k8s.io/gateway-name: namespace + istio.io/dataplane-mode: none + name: namespace-istio + namespace: default + ownerReferences: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + name: namespace + uid: "" +spec: + replicas: 4 + selector: + matchLabels: + gateway.networking.k8s.io/gateway-name: namespace + template: + metadata: + annotations: + istio.io/rev: default + prometheus.io/path: /stats/prometheus + prometheus.io/port: "15020" + prometheus.io/scrape: "true" + labels: + foo: bar + gateway.istio.io/managed: istio.io-gateway-controller + gateway.networking.k8s.io/gateway-name: namespace + istio.io/dataplane-mode: none + service.istio.io/canonical-name: namespace-istio + service.istio.io/canonical-revision: latest + sidecar.istio.io/inject: "false" + spec: + containers: + - args: + - proxy + - router + - --domain + - $(POD_NAMESPACE).svc. + - --proxyLogLevel + - + - --proxyComponentLogLevel + - + - --log_output_level + - + env: + - name: PILOT_CERT_PROVIDER + value: + - name: CA_ADDR + value: istiod-..svc:15012 + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: INSTANCE_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: SERVICE_ACCOUNT + valueFrom: + fieldRef: + fieldPath: spec.serviceAccountName + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: ISTIO_CPU_LIMIT + valueFrom: + resourceFieldRef: + resource: limits.cpu + - name: PROXY_CONFIG + value: | + {} + - name: ISTIO_META_POD_PORTS + value: '[]' + - name: ISTIO_META_APP_CONTAINERS + value: "" + - name: GOMEMLIMIT + valueFrom: + resourceFieldRef: + resource: limits.memory + - name: GOMAXPROCS + valueFrom: + resourceFieldRef: + resource: limits.cpu + - name: ISTIO_META_CLUSTER_ID + value: Kubernetes + - name: ISTIO_META_NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: ISTIO_META_INTERCEPTION_MODE + value: REDIRECT + - name: ISTIO_META_WORKLOAD_NAME + value: namespace-istio + - name: ISTIO_META_OWNER + value: kubernetes://apis/apps/v1/namespaces/default/deployments/namespace-istio + - name: ISTIO_META_MESH_ID + value: cluster.local + - name: TRUST_DOMAIN + value: cluster.local + image: test/proxyv2:test + name: istio-proxy + ports: + - containerPort: 15020 + name: metrics + protocol: TCP + - containerPort: 15021 + name: status-port + protocol: TCP + - containerPort: 15090 + name: http-envoy-prom + protocol: TCP + readinessProbe: + failureThreshold: 4 + httpGet: + path: /healthz/ready + port: 15021 + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 15 + successThreshold: 1 + timeoutSeconds: 1 + resources: + requests: + cpu: 222m + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + readOnlyRootFilesystem: true + runAsGroup: 1337 + runAsNonRoot: true + runAsUser: 1337 + startupProbe: + failureThreshold: 30 + httpGet: + path: /healthz/ready + port: 15021 + scheme: HTTP + initialDelaySeconds: 1 + periodSeconds: 1 + successThreshold: 1 + timeoutSeconds: 1 + volumeMounts: + - mountPath: /var/run/secrets/workload-spiffe-uds + name: workload-socket + - mountPath: /var/run/secrets/credential-uds + name: credential-socket + - mountPath: /var/run/secrets/workload-spiffe-credentials + name: workload-certs + - mountPath: /var/lib/istio/data + name: istio-data + - mountPath: /etc/istio/proxy + name: istio-envoy + - mountPath: /var/run/secrets/tokens + name: istio-token + - mountPath: /etc/istio/pod + name: istio-podinfo + securityContext: + sysctls: + - name: net.ipv4.ip_unprivileged_port_start + value: "0" + serviceAccountName: namespace-istio + volumes: + - emptyDir: {} + name: workload-socket + - emptyDir: {} + name: credential-socket + - emptyDir: {} + name: workload-certs + - emptyDir: + medium: Memory + name: istio-envoy + - emptyDir: {} + name: istio-data + - downwardAPI: + items: + - fieldRef: + fieldPath: metadata.labels + path: labels + - fieldRef: + fieldPath: metadata.annotations + path: annotations + name: istio-podinfo + - name: istio-token + projected: + sources: + - serviceAccountToken: + audience: + expirationSeconds: 43200 + path: istio-token +--- +apiVersion: v1 +kind: Service +metadata: + annotations: {} + labels: + foo: bar + gateway.istio.io/managed: istio.io-gateway-controller + gateway.networking.k8s.io/gateway-name: namespace + istio.io/dataplane-mode: none + name: namespace-istio + namespace: default + ownerReferences: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + name: namespace + uid: null +spec: + ipFamilyPolicy: PreferDualStack + ports: + - appProtocol: tcp + name: status-port + port: 15021 + protocol: TCP + selector: + gateway.networking.k8s.io/gateway-name: namespace + type: LoadBalancer +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + annotations: {} + labels: + foo: bar + gateway.istio.io/managed: istio.io-gateway-controller + gateway.networking.k8s.io/gateway-name: namespace + istio.io/dataplane-mode: none + name: namespace-istio + namespace: default + ownerReferences: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + name: namespace + uid: "" +spec: + maxReplicas: 2 + minReplicas: 2 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: namespace-istio +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + annotations: {} + labels: + foo: bar + gateway.istio.io/managed: istio.io-gateway-controller + gateway.networking.k8s.io/gateway-name: namespace + istio.io/dataplane-mode: none + name: namespace-istio + namespace: default + ownerReferences: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + name: namespace + uid: "" +spec: + minAvailable: 1 + selector: + matchLabels: + gateway.networking.k8s.io/gateway-name: namespace +--- diff --git a/pilot/pkg/config/kube/gateway/testdata/deployment/illegal_customizations.yaml b/pilot/pkg/config/kube/gateway/testdata/deployment/illegal_customizations.yaml new file mode 100644 index 0000000000..594973fbfa --- /dev/null +++ b/pilot/pkg/config/kube/gateway/testdata/deployment/illegal_customizations.yaml @@ -0,0 +1,6 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: Gateway +metadata: + annotations: + gateway.istio.io/controller-version: "5" +--- diff --git a/pilot/pkg/serviceregistry/kube/controller/ambient/policies.go b/pilot/pkg/serviceregistry/kube/controller/ambient/policies.go index 6068857383..5bbf12f814 100644 --- a/pilot/pkg/serviceregistry/kube/controller/ambient/policies.go +++ b/pilot/pkg/serviceregistry/kube/controller/ambient/policies.go @@ -17,11 +17,13 @@ package ambient import ( "fmt" + "strconv" "strings" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/gateway-api/apis/v1beta1" + "istio.io/api/annotation" networkingclient "istio.io/client-go/pkg/apis/networking/v1" securityclient "istio.io/client-go/pkg/apis/security/v1" "istio.io/istio/pilot/pkg/model" @@ -151,6 +153,10 @@ func PolicyCollections( flags FeatureFlags, ) (krt.Collection[model.WorkloadAuthorization], krt.Collection[model.WorkloadAuthorization]) { AuthzDerivedPolicies := krt.NewCollection(authzPolicies, func(ctx krt.HandlerContext, i *securityclient.AuthorizationPolicy) *model.WorkloadAuthorization { + dryRun, _ := strconv.ParseBool(i.Annotations[annotation.IoIstioDryRun.Name]) + if dryRun { + return nil + } meshCfg := krt.FetchOne(ctx, meshConfig.AsCollection()) pol, status := convertAuthorizationPolicy(meshCfg.GetRootNamespace(), i) if status == nil && pol == nil { diff --git a/pkg/config/schema/collections/collections.gen.go b/pkg/config/schema/collections/collections.gen.go index c1e773981c..971704c198 100755 --- a/pkg/config/schema/collections/collections.gen.go +++ b/pkg/config/schema/collections/collections.gen.go @@ -10,11 +10,13 @@ import ( k8sioapiadmissionregistrationv1 "k8s.io/api/admissionregistration/v1" k8sioapiappsv1 "k8s.io/api/apps/v1" + k8sioapiautoscalingv2 "k8s.io/api/autoscaling/v2" k8sioapicertificatesv1 "k8s.io/api/certificates/v1" k8sioapicoordinationv1 "k8s.io/api/coordination/v1" k8sioapicorev1 "k8s.io/api/core/v1" k8sioapidiscoveryv1 "k8s.io/api/discovery/v1" k8sioapinetworkingv1 "k8s.io/api/networking/v1" + k8sioapipolicyv1 "k8s.io/api/policy/v1" k8sioapiextensionsapiserverpkgapisapiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" sigsk8siogatewayapiapisv1 "sigs.k8s.io/gateway-api/apis/v1" sigsk8siogatewayapiapisv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" @@ -266,6 +268,21 @@ var ( ValidateProto: validation.EmptyValidate, }.MustBuild() + HorizontalPodAutoscaler = resource.Builder{ + Identifier: "HorizontalPodAutoscaler", + Group: "autoscaling", + Kind: "HorizontalPodAutoscaler", + Plural: "horizontalpodautoscalers", + Version: "v2", + Proto: "k8s.io.api.autoscaling.v2.HorizontalPodAutoscalerSpec", StatusProto: "k8s.io.api.autoscaling.v2.HorizontalPodAutoscalerStatus", + ReflectType: reflect.TypeOf(&k8sioapiautoscalingv2.HorizontalPodAutoscalerSpec{}).Elem(), StatusType: reflect.TypeOf(&k8sioapiautoscalingv2.HorizontalPodAutoscalerStatus{}).Elem(), + ProtoPackage: "k8s.io/api/autoscaling/v2", StatusPackage: "k8s.io/api/autoscaling/v2", + ClusterScoped: false, + Synthetic: false, + Builtin: true, + ValidateProto: validation.EmptyValidate, + }.MustBuild() + Ingress = resource.Builder{ Identifier: "Ingress", Group: "networking.k8s.io", @@ -438,6 +455,21 @@ var ( ValidateProto: validation.EmptyValidate, }.MustBuild() + PodDisruptionBudget = resource.Builder{ + Identifier: "PodDisruptionBudget", + Group: "policy", + Kind: "PodDisruptionBudget", + Plural: "poddisruptionbudgets", + Version: "v1", + Proto: "k8s.io.api.policy.v1.PodDisruptionBudgetSpec", StatusProto: "k8s.io.api.policy.v1.PodDisruptionBudgetStatus", + ReflectType: reflect.TypeOf(&k8sioapipolicyv1.PodDisruptionBudgetSpec{}).Elem(), StatusType: reflect.TypeOf(&k8sioapipolicyv1.PodDisruptionBudgetStatus{}).Elem(), + ProtoPackage: "k8s.io/api/policy/v1", StatusPackage: "k8s.io/api/policy/v1", + ClusterScoped: false, + Synthetic: false, + Builtin: true, + ValidateProto: validation.EmptyValidate, + }.MustBuild() + ProxyConfig = resource.Builder{ Identifier: "ProxyConfig", Group: "networking.istio.io", @@ -753,6 +785,7 @@ var ( MustAdd(Gateway). MustAdd(GatewayClass). MustAdd(HTTPRoute). + MustAdd(HorizontalPodAutoscaler). MustAdd(Ingress). MustAdd(IngressClass). MustAdd(KubernetesGateway). @@ -764,6 +797,7 @@ var ( MustAdd(Node). MustAdd(PeerAuthentication). MustAdd(Pod). + MustAdd(PodDisruptionBudget). MustAdd(ProxyConfig). MustAdd(ReferenceGrant). MustAdd(RequestAuthentication). @@ -796,6 +830,7 @@ var ( MustAdd(GRPCRoute). MustAdd(GatewayClass). MustAdd(HTTPRoute). + MustAdd(HorizontalPodAutoscaler). MustAdd(Ingress). MustAdd(IngressClass). MustAdd(KubernetesGateway). @@ -804,6 +839,7 @@ var ( MustAdd(Namespace). MustAdd(Node). MustAdd(Pod). + MustAdd(PodDisruptionBudget). MustAdd(ReferenceGrant). MustAdd(Secret). MustAdd(Service). diff --git a/pkg/config/schema/gvk/resources.gen.go b/pkg/config/schema/gvk/resources.gen.go index babb548165..9e0c973316 100755 --- a/pkg/config/schema/gvk/resources.gen.go +++ b/pkg/config/schema/gvk/resources.gen.go @@ -34,6 +34,7 @@ var ( HTTPRoute = config.GroupVersionKind{Group: "gateway.networking.k8s.io", Version: "v1beta1", Kind: "HTTPRoute"} HTTPRoute_v1alpha2 = config.GroupVersionKind{Group: "gateway.networking.k8s.io", Version: "v1alpha2", Kind: "HTTPRoute"} HTTPRoute_v1 = config.GroupVersionKind{Group: "gateway.networking.k8s.io", Version: "v1", Kind: "HTTPRoute"} + HorizontalPodAutoscaler = config.GroupVersionKind{Group: "autoscaling", Version: "v2", Kind: "HorizontalPodAutoscaler"} Ingress = config.GroupVersionKind{Group: "networking.k8s.io", Version: "v1", Kind: "Ingress"} IngressClass = config.GroupVersionKind{Group: "networking.k8s.io", Version: "v1", Kind: "IngressClass"} KubernetesGateway = config.GroupVersionKind{Group: "gateway.networking.k8s.io", Version: "v1beta1", Kind: "Gateway"} @@ -48,6 +49,7 @@ var ( PeerAuthentication = config.GroupVersionKind{Group: "security.istio.io", Version: "v1", Kind: "PeerAuthentication"} PeerAuthentication_v1beta1 = config.GroupVersionKind{Group: "security.istio.io", Version: "v1beta1", Kind: "PeerAuthentication"} Pod = config.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"} + PodDisruptionBudget = config.GroupVersionKind{Group: "policy", Version: "v1", Kind: "PodDisruptionBudget"} ProxyConfig = config.GroupVersionKind{Group: "networking.istio.io", Version: "v1beta1", Kind: "ProxyConfig"} ReferenceGrant = config.GroupVersionKind{Group: "gateway.networking.k8s.io", Version: "v1beta1", Kind: "ReferenceGrant"} ReferenceGrant_v1alpha2 = config.GroupVersionKind{Group: "gateway.networking.k8s.io", Version: "v1alpha2", Kind: "ReferenceGrant"} @@ -132,6 +134,8 @@ func ToGVR(g config.GroupVersionKind) (schema.GroupVersionResource, bool) { return gvr.HTTPRoute_v1alpha2, true case HTTPRoute_v1: return gvr.HTTPRoute_v1, true + case HorizontalPodAutoscaler: + return gvr.HorizontalPodAutoscaler, true case Ingress: return gvr.Ingress, true case IngressClass: @@ -160,6 +164,8 @@ func ToGVR(g config.GroupVersionKind) (schema.GroupVersionResource, bool) { return gvr.PeerAuthentication_v1beta1, true case Pod: return gvr.Pod, true + case PodDisruptionBudget: + return gvr.PodDisruptionBudget, true case ProxyConfig: return gvr.ProxyConfig, true case ReferenceGrant: @@ -268,6 +274,8 @@ func FromGVR(g schema.GroupVersionResource) (config.GroupVersionKind, bool) { return GatewayClass, true case gvr.HTTPRoute: return HTTPRoute, true + case gvr.HorizontalPodAutoscaler: + return HorizontalPodAutoscaler, true case gvr.Ingress: return Ingress, true case gvr.IngressClass: @@ -290,6 +298,8 @@ func FromGVR(g schema.GroupVersionResource) (config.GroupVersionKind, bool) { return PeerAuthentication, true case gvr.Pod: return Pod, true + case gvr.PodDisruptionBudget: + return PodDisruptionBudget, true case gvr.ProxyConfig: return ProxyConfig, true case gvr.ReferenceGrant: diff --git a/pkg/config/schema/gvr/resources.gen.go b/pkg/config/schema/gvr/resources.gen.go index 5f34019fe8..16f3a70872 100755 --- a/pkg/config/schema/gvr/resources.gen.go +++ b/pkg/config/schema/gvr/resources.gen.go @@ -31,6 +31,7 @@ var ( HTTPRoute = schema.GroupVersionResource{Group: "gateway.networking.k8s.io", Version: "v1beta1", Resource: "httproutes"} HTTPRoute_v1alpha2 = schema.GroupVersionResource{Group: "gateway.networking.k8s.io", Version: "v1alpha2", Resource: "httproutes"} HTTPRoute_v1 = schema.GroupVersionResource{Group: "gateway.networking.k8s.io", Version: "v1", Resource: "httproutes"} + HorizontalPodAutoscaler = schema.GroupVersionResource{Group: "autoscaling", Version: "v2", Resource: "horizontalpodautoscalers"} Ingress = schema.GroupVersionResource{Group: "networking.k8s.io", Version: "v1", Resource: "ingresses"} IngressClass = schema.GroupVersionResource{Group: "networking.k8s.io", Version: "v1", Resource: "ingressclasses"} KubernetesGateway = schema.GroupVersionResource{Group: "gateway.networking.k8s.io", Version: "v1beta1", Resource: "gateways"} @@ -45,6 +46,7 @@ var ( PeerAuthentication = schema.GroupVersionResource{Group: "security.istio.io", Version: "v1", Resource: "peerauthentications"} PeerAuthentication_v1beta1 = schema.GroupVersionResource{Group: "security.istio.io", Version: "v1beta1", Resource: "peerauthentications"} Pod = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"} + PodDisruptionBudget = schema.GroupVersionResource{Group: "policy", Version: "v1", Resource: "poddisruptionbudgets"} ProxyConfig = schema.GroupVersionResource{Group: "networking.istio.io", Version: "v1beta1", Resource: "proxyconfigs"} ReferenceGrant = schema.GroupVersionResource{Group: "gateway.networking.k8s.io", Version: "v1beta1", Resource: "referencegrants"} ReferenceGrant_v1alpha2 = schema.GroupVersionResource{Group: "gateway.networking.k8s.io", Version: "v1alpha2", Resource: "referencegrants"} @@ -132,6 +134,8 @@ func IsClusterScoped(g schema.GroupVersionResource) bool { return false case HTTPRoute_v1: return false + case HorizontalPodAutoscaler: + return false case Ingress: return false case IngressClass: @@ -156,6 +160,8 @@ func IsClusterScoped(g schema.GroupVersionResource) bool { return false case Pod: return false + case PodDisruptionBudget: + return false case ProxyConfig: return false case ReferenceGrant: diff --git a/pkg/config/schema/kind/resources.gen.go b/pkg/config/schema/kind/resources.gen.go index a874fdd75c..22333a075a 100755 --- a/pkg/config/schema/kind/resources.gen.go +++ b/pkg/config/schema/kind/resources.gen.go @@ -24,6 +24,7 @@ const ( Gateway GatewayClass HTTPRoute + HorizontalPodAutoscaler Ingress IngressClass KubernetesGateway @@ -35,6 +36,7 @@ const ( Node PeerAuthentication Pod + PodDisruptionBudget ProxyConfig ReferenceGrant RequestAuthentication @@ -89,6 +91,8 @@ func (k Kind) String() string { return "GatewayClass" case HTTPRoute: return "HTTPRoute" + case HorizontalPodAutoscaler: + return "HorizontalPodAutoscaler" case Ingress: return "Ingress" case IngressClass: @@ -111,6 +115,8 @@ func (k Kind) String() string { return "PeerAuthentication" case Pod: return "Pod" + case PodDisruptionBudget: + return "PodDisruptionBudget" case ProxyConfig: return "ProxyConfig" case ReferenceGrant: @@ -182,6 +188,8 @@ func MustFromGVK(g config.GroupVersionKind) Kind { return GatewayClass case gvk.HTTPRoute: return HTTPRoute + case gvk.HorizontalPodAutoscaler: + return HorizontalPodAutoscaler case gvk.Ingress: return Ingress case gvk.IngressClass: @@ -204,6 +212,8 @@ func MustFromGVK(g config.GroupVersionKind) Kind { return PeerAuthentication case gvk.Pod: return Pod + case gvk.PodDisruptionBudget: + return PodDisruptionBudget case gvk.ProxyConfig: return ProxyConfig case gvk.ReferenceGrant: diff --git a/pkg/config/schema/kubeclient/resources.gen.go b/pkg/config/schema/kubeclient/resources.gen.go index 3a5ec5bb8c..d6b40c5d79 100755 --- a/pkg/config/schema/kubeclient/resources.gen.go +++ b/pkg/config/schema/kubeclient/resources.gen.go @@ -19,11 +19,13 @@ import ( k8sioapiadmissionregistrationv1 "k8s.io/api/admissionregistration/v1" k8sioapiappsv1 "k8s.io/api/apps/v1" + k8sioapiautoscalingv2 "k8s.io/api/autoscaling/v2" k8sioapicertificatesv1 "k8s.io/api/certificates/v1" k8sioapicoordinationv1 "k8s.io/api/coordination/v1" k8sioapicorev1 "k8s.io/api/core/v1" k8sioapidiscoveryv1 "k8s.io/api/discovery/v1" k8sioapinetworkingv1 "k8s.io/api/networking/v1" + k8sioapipolicyv1 "k8s.io/api/policy/v1" k8sioapiextensionsapiserverpkgapisapiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" sigsk8siogatewayapiapisv1 "sigs.k8s.io/gateway-api/apis/v1" sigsk8siogatewayapiapisv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" @@ -67,6 +69,8 @@ func GetWriteClient[T runtime.Object](c ClientGetter, namespace string) ktypes.W return c.GatewayAPI().GatewayV1beta1().GatewayClasses().(ktypes.WriteAPI[T]) case *sigsk8siogatewayapiapisv1beta1.HTTPRoute: return c.GatewayAPI().GatewayV1beta1().HTTPRoutes(namespace).(ktypes.WriteAPI[T]) + case *k8sioapiautoscalingv2.HorizontalPodAutoscaler: + return c.Kube().AutoscalingV2().HorizontalPodAutoscalers(namespace).(ktypes.WriteAPI[T]) case *k8sioapinetworkingv1.Ingress: return c.Kube().NetworkingV1().Ingresses(namespace).(ktypes.WriteAPI[T]) case *k8sioapinetworkingv1.IngressClass: @@ -85,6 +89,8 @@ func GetWriteClient[T runtime.Object](c ClientGetter, namespace string) ktypes.W return c.Istio().SecurityV1().PeerAuthentications(namespace).(ktypes.WriteAPI[T]) case *k8sioapicorev1.Pod: return c.Kube().CoreV1().Pods(namespace).(ktypes.WriteAPI[T]) + case *k8sioapipolicyv1.PodDisruptionBudget: + return c.Kube().PolicyV1().PodDisruptionBudgets(namespace).(ktypes.WriteAPI[T]) case *apiistioioapinetworkingv1beta1.ProxyConfig: return c.Istio().NetworkingV1beta1().ProxyConfigs(namespace).(ktypes.WriteAPI[T]) case *sigsk8siogatewayapiapisv1beta1.ReferenceGrant: @@ -156,6 +162,8 @@ func GetClient[T, TL runtime.Object](c ClientGetter, namespace string) ktypes.Re return c.GatewayAPI().GatewayV1beta1().GatewayClasses().(ktypes.ReadWriteAPI[T, TL]) case *sigsk8siogatewayapiapisv1beta1.HTTPRoute: return c.GatewayAPI().GatewayV1beta1().HTTPRoutes(namespace).(ktypes.ReadWriteAPI[T, TL]) + case *k8sioapiautoscalingv2.HorizontalPodAutoscaler: + return c.Kube().AutoscalingV2().HorizontalPodAutoscalers(namespace).(ktypes.ReadWriteAPI[T, TL]) case *k8sioapinetworkingv1.Ingress: return c.Kube().NetworkingV1().Ingresses(namespace).(ktypes.ReadWriteAPI[T, TL]) case *k8sioapinetworkingv1.IngressClass: @@ -174,6 +182,8 @@ func GetClient[T, TL runtime.Object](c ClientGetter, namespace string) ktypes.Re return c.Istio().SecurityV1().PeerAuthentications(namespace).(ktypes.ReadWriteAPI[T, TL]) case *k8sioapicorev1.Pod: return c.Kube().CoreV1().Pods(namespace).(ktypes.ReadWriteAPI[T, TL]) + case *k8sioapipolicyv1.PodDisruptionBudget: + return c.Kube().PolicyV1().PodDisruptionBudgets(namespace).(ktypes.ReadWriteAPI[T, TL]) case *apiistioioapinetworkingv1beta1.ProxyConfig: return c.Istio().NetworkingV1beta1().ProxyConfigs(namespace).(ktypes.ReadWriteAPI[T, TL]) case *sigsk8siogatewayapiapisv1beta1.ReferenceGrant: @@ -245,6 +255,8 @@ func gvrToObject(g schema.GroupVersionResource) runtime.Object { return &sigsk8siogatewayapiapisv1beta1.GatewayClass{} case gvr.HTTPRoute: return &sigsk8siogatewayapiapisv1beta1.HTTPRoute{} + case gvr.HorizontalPodAutoscaler: + return &k8sioapiautoscalingv2.HorizontalPodAutoscaler{} case gvr.Ingress: return &k8sioapinetworkingv1.Ingress{} case gvr.IngressClass: @@ -263,6 +275,8 @@ func gvrToObject(g schema.GroupVersionResource) runtime.Object { return &apiistioioapisecurityv1.PeerAuthentication{} case gvr.Pod: return &k8sioapicorev1.Pod{} + case gvr.PodDisruptionBudget: + return &k8sioapipolicyv1.PodDisruptionBudget{} case gvr.ProxyConfig: return &apiistioioapinetworkingv1beta1.ProxyConfig{} case gvr.ReferenceGrant: @@ -407,6 +421,13 @@ func getInformerFiltered(c ClientGetter, opts ktypes.InformerOptions, g schema.G w = func(options metav1.ListOptions) (watch.Interface, error) { return c.GatewayAPI().GatewayV1beta1().HTTPRoutes(opts.Namespace).Watch(context.Background(), options) } + case gvr.HorizontalPodAutoscaler: + l = func(options metav1.ListOptions) (runtime.Object, error) { + return c.Kube().AutoscalingV2().HorizontalPodAutoscalers(opts.Namespace).List(context.Background(), options) + } + w = func(options metav1.ListOptions) (watch.Interface, error) { + return c.Kube().AutoscalingV2().HorizontalPodAutoscalers(opts.Namespace).Watch(context.Background(), options) + } case gvr.Ingress: l = func(options metav1.ListOptions) (runtime.Object, error) { return c.Kube().NetworkingV1().Ingresses(opts.Namespace).List(context.Background(), options) @@ -470,6 +491,13 @@ func getInformerFiltered(c ClientGetter, opts ktypes.InformerOptions, g schema.G w = func(options metav1.ListOptions) (watch.Interface, error) { return c.Kube().CoreV1().Pods(opts.Namespace).Watch(context.Background(), options) } + case gvr.PodDisruptionBudget: + l = func(options metav1.ListOptions) (runtime.Object, error) { + return c.Kube().PolicyV1().PodDisruptionBudgets(opts.Namespace).List(context.Background(), options) + } + w = func(options metav1.ListOptions) (watch.Interface, error) { + return c.Kube().PolicyV1().PodDisruptionBudgets(opts.Namespace).Watch(context.Background(), options) + } case gvr.ProxyConfig: l = func(options metav1.ListOptions) (runtime.Object, error) { return c.Istio().NetworkingV1beta1().ProxyConfigs(opts.Namespace).List(context.Background(), options) diff --git a/pkg/config/schema/kubetypes/resources.gen.go b/pkg/config/schema/kubetypes/resources.gen.go index 2aa4c7554f..4842f1ffe3 100755 --- a/pkg/config/schema/kubetypes/resources.gen.go +++ b/pkg/config/schema/kubetypes/resources.gen.go @@ -5,11 +5,13 @@ package kubetypes import ( k8sioapiadmissionregistrationv1 "k8s.io/api/admissionregistration/v1" k8sioapiappsv1 "k8s.io/api/apps/v1" + k8sioapiautoscalingv2 "k8s.io/api/autoscaling/v2" k8sioapicertificatesv1 "k8s.io/api/certificates/v1" k8sioapicoordinationv1 "k8s.io/api/coordination/v1" k8sioapicorev1 "k8s.io/api/core/v1" k8sioapidiscoveryv1 "k8s.io/api/discovery/v1" k8sioapinetworkingv1 "k8s.io/api/networking/v1" + k8sioapipolicyv1 "k8s.io/api/policy/v1" k8sioapiextensionsapiserverpkgapisapiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" sigsk8siogatewayapiapisv1 "sigs.k8s.io/gateway-api/apis/v1" sigsk8siogatewayapiapisv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" @@ -69,6 +71,8 @@ func getGvk(obj any) (config.GroupVersionKind, bool) { return gvk.GatewayClass, true case *sigsk8siogatewayapiapisv1beta1.HTTPRoute: return gvk.HTTPRoute, true + case *k8sioapiautoscalingv2.HorizontalPodAutoscaler: + return gvk.HorizontalPodAutoscaler, true case *k8sioapinetworkingv1.Ingress: return gvk.Ingress, true case *k8sioapinetworkingv1.IngressClass: @@ -93,6 +97,8 @@ func getGvk(obj any) (config.GroupVersionKind, bool) { return gvk.PeerAuthentication, true case *k8sioapicorev1.Pod: return gvk.Pod, true + case *k8sioapipolicyv1.PodDisruptionBudget: + return gvk.PodDisruptionBudget, true case *istioioapinetworkingv1beta1.ProxyConfig: return gvk.ProxyConfig, true case *apiistioioapinetworkingv1beta1.ProxyConfig: diff --git a/pkg/config/schema/metadata.yaml b/pkg/config/schema/metadata.yaml index bcbe5742fa..4856c593f7 100644 --- a/pkg/config/schema/metadata.yaml +++ b/pkg/config/schema/metadata.yaml @@ -178,25 +178,25 @@ resources: proto: "k8s.io.api.coordination.v1.LeaseSpec" protoPackage: "k8s.io/api/coordination/v1" -# - kind: "HorizontalPodAutoscaler" -# plural: "horizontalpodautoscalers" -# group: "autoscaling" -# version: "v2" -# builtin: true -# proto: "k8s.io.api.autoscaling.v2.HorizontalPodAutoscalerSpec" -# protoPackage: "k8s.io/api/autoscaling/v2" -# statusProto: "k8s.io.api.autoscaling.v2.HorizontalPodAutoscalerStatus" -# statusProtoPackage: "k8s.io/api/autoscaling/v2" -# -# - kind: "PodDisruptionBudget" -# plural: "poddisruptionbudgets" -# group: "policy" -# version: "v1" -# builtin: true -# proto: "k8s.io.api.policy.v1.PodDisruptionBudgetSpec" -# protoPackage: "k8s.io/api/policy/v1" -# statusProto: "k8s.io.api.policy.v1.PodDisruptionBudgetStatus" -# statusProtoPackage: "k8s.io/api/policy/v1" + - kind: "HorizontalPodAutoscaler" + plural: "horizontalpodautoscalers" + group: "autoscaling" + version: "v2" + builtin: true + proto: "k8s.io.api.autoscaling.v2.HorizontalPodAutoscalerSpec" + protoPackage: "k8s.io/api/autoscaling/v2" + statusProto: "k8s.io.api.autoscaling.v2.HorizontalPodAutoscalerStatus" + statusProtoPackage: "k8s.io/api/autoscaling/v2" + + - kind: "PodDisruptionBudget" + plural: "poddisruptionbudgets" + group: "policy" + version: "v1" + builtin: true + proto: "k8s.io.api.policy.v1.PodDisruptionBudgetSpec" + protoPackage: "k8s.io/api/policy/v1" + statusProto: "k8s.io.api.policy.v1.PodDisruptionBudgetStatus" + statusProtoPackage: "k8s.io/api/policy/v1" # # - kind: "ClusterRole" # plural: "clusterroles" diff --git a/pkg/kube/controllers/common.go b/pkg/kube/controllers/common.go index 742c7588a8..9c6b5f7189 100644 --- a/pkg/kube/controllers/common.go +++ b/pkg/kube/controllers/common.go @@ -15,6 +15,7 @@ package controllers import ( + "cmp" "fmt" kerrors "k8s.io/apimachinery/pkg/api/errors" @@ -28,6 +29,7 @@ import ( "istio.io/istio/pkg/config" "istio.io/istio/pkg/config/schema/gvk" istiolog "istio.io/istio/pkg/log" + "istio.io/istio/pkg/slices" ) var log = istiolog.RegisterScope("controllers", "common controller logic") @@ -340,3 +342,18 @@ func ShutdownAll(s ...Shutdowner) { h.ShutdownHandlers() } } + +func OldestObject[T Object](configs []T) T { + return slices.MinFunc(configs, func(i, j T) int { + if r := i.GetCreationTimestamp().Compare(j.GetCreationTimestamp().Time); r != 0 { + return r + } + // If creation time is the same, then behavior is nondeterministic. In this case, we can + // pick an arbitrary but consistent ordering based on name and namespace, which is unique. + // CreationTimestamp is stored in seconds, so this is not uncommon. + if r := cmp.Compare(i.GetName(), j.GetName()); r != 0 { + return r + } + return cmp.Compare(i.GetNamespace(), j.GetNamespace()) + }) +} diff --git a/pkg/kube/krt/collection.go b/pkg/kube/krt/collection.go index 0dca7bc27d..682e7dcfd5 100644 --- a/pkg/kube/krt/collection.go +++ b/pkg/kube/krt/collection.go @@ -28,28 +28,55 @@ import ( "istio.io/istio/pkg/util/sets" ) +type indexedDependencyType uint8 + +const ( + unknownIndexType indexedDependencyType = iota + indexType indexedDependencyType = iota + getKeyType indexedDependencyType = iota +) + +var allIndexedDependencyTypes = []indexedDependencyType{indexType, getKeyType} + type dependencyState[I any] struct { // collectionDependencies specifies the set of collections we depend on from within the transformation functions (via Fetch). // These are keyed by the internal uid() function on collections. // Note this does not include `parent`, which is the *primary* dependency declared outside of transformation functions. collectionDependencies sets.Set[collectionUID] // Stores a map of I -> secondary dependencies (added via Fetch) - objectDependencies map[Key[I]][]*dependency - indexedDependencies map[indexedDependency]sets.Set[Key[I]] - indexedDependenciesExtractor map[collectionUID]objectKeyExtractor + objectDependencies map[Key[I]][]*dependency + indexedDependencies map[indexedDependency]sets.Set[Key[I]] + // indexedDependenciesExtractor stores a map of [collection,fetch type] => an extractor to get change keys. + // Note that a given collection can have multiple Fetches, but they are limited to those of the different kind. + // I.e. you can do a `Fetch(c1, FilterIndex()) + Fetch(c1, FilterKey())` but not `Fetch(c1, FilterIndex()) + Fetch(c1, FilterIndex())`. + // This only applies within a single transformation; it is fine to fetch the the same `c1` in any way from different collections. + indexedDependenciesExtractor map[extractorKey]objectKeyExtractor +} + +type extractorKey struct { + uid collectionUID + typ indexedDependencyType } func (i dependencyState[I]) update(key Key[I], deps []*dependency) { // Update the I -> Dependency mapping i.objectDependencies[key] = deps for _, d := range deps { - if depKey, extractor, ok := d.filter.reverseIndexKey(); ok { - k := indexedDependency{ - id: d.id, - key: depKey, + if depKeys, typ, extractor, ok := d.filter.reverseIndexKey(); ok { + for _, depKey := range depKeys { + k := indexedDependency{ + id: d.id, + key: depKey, + typ: typ, + } + sets.InsertOrNew(i.indexedDependencies, k, key) + kk := extractorKey{ + uid: d.id, + typ: typ, + } + + i.indexedDependenciesExtractor[kk] = extractor } - sets.InsertOrNew(i.indexedDependencies, k, key) - i.indexedDependenciesExtractor[d.id] = extractor } } } @@ -61,12 +88,15 @@ func (i dependencyState[I]) delete(key Key[I]) { } delete(i.objectDependencies, key) for _, d := range old { - if depKey, _, ok := d.filter.reverseIndexKey(); ok { - k := indexedDependency{ - id: d.id, - key: depKey, + if depKeys, typ, _, ok := d.filter.reverseIndexKey(); ok { + for _, depKey := range depKeys { + k := indexedDependency{ + id: d.id, + key: depKey, + typ: typ, + } + sets.DeleteCleanupLast(i.indexedDependencies, k, key) } - sets.DeleteCleanupLast(i.indexedDependencies, k, key) } } } @@ -79,25 +109,31 @@ func (i dependencyState[I]) changedInputKeys(sourceCollection collectionUID, eve // Naively, we can look through every item in this collection and check if it matches the filter. However, this is // inefficient, especially when the dependency changes frequently and the collection is large. // Where possible, we utilize the reverse-indexing to get the precise list of potentially changed objects. - if extractor, f := i.indexedDependenciesExtractor[sourceCollection]; f { - // We have a reverse index - for _, item := range ev.Items() { - // Find all the reverse index keys for this object. For each key we will find impacted input objects. - keys := extractor(item) - for _, key := range keys { - for iKey := range i.indexedDependencies[indexedDependency{id: sourceCollection, key: key}] { - if changedInputKeys.Contains(iKey) { - // We may have already found this item, skip it - continue - } - dependencies := i.objectDependencies[iKey] - if changed := objectChanged(dependencies, sourceCollection, ev, true); changed { - changedInputKeys.Insert(iKey) + foundAny := false + for _, idxTypes := range allIndexedDependencyTypes { + ekey := extractorKey{uid: sourceCollection, typ: idxTypes} + if extractor, f := i.indexedDependenciesExtractor[ekey]; f { + foundAny = true + // We have a reverse index + for _, item := range ev.Items() { + // Find all the reverse index keys for this object. For each key we will find impacted input objects. + keys := extractor(item) + for _, key := range keys { + for iKey := range i.indexedDependencies[indexedDependency{id: sourceCollection, key: key, typ: idxTypes}] { + if changedInputKeys.Contains(iKey) { + // We may have already found this item, skip it + continue + } + dependencies := i.objectDependencies[iKey] + if changed := objectChanged(dependencies, sourceCollection, ev, true); changed { + changedInputKeys.Insert(iKey) + } } } } } - } else { + } + if !foundAny { for iKey, dependencies := range i.objectDependencies { if changed := objectChanged(dependencies, sourceCollection, ev, false); changed { changedInputKeys.Insert(iKey) @@ -537,7 +573,7 @@ func newManyCollection[I, O any]( collectionDependencies: sets.New[collectionUID](), objectDependencies: map[Key[I]][]*dependency{}, indexedDependencies: map[indexedDependency]sets.Set[Key[I]]{}, - indexedDependenciesExtractor: map[collectionUID]func(o any) []string{}, + indexedDependenciesExtractor: map[extractorKey]func(o any) []string{}, }, collectionState: multiIndex[I, O]{ inputs: map[Key[I]]I{}, @@ -599,7 +635,7 @@ func (h *manyCollection[I, O]) onSecondaryDependencyEvent(sourceCollection colle // A secondary dependency changed... // Got an event. Now we need to find out who depends on it.. changedInputKeys := h.dependencyState.changedInputKeys(sourceCollection, events) - h.log.Debugf("event size %v, impacts %v objects", len(events), len(changedInputKeys)) + h.log.Debugf("event size %v, impacts %v objects", len(events), changedInputKeys.UnsortedList()) toRun := make([]Event[I], 0, len(changedInputKeys)) // Now we have the set of input keys that changed. We need to recompute all of these. diff --git a/pkg/kube/krt/filter.go b/pkg/kube/krt/filter.go index 56d0536c99..294dfe98d3 100644 --- a/pkg/kube/krt/filter.go +++ b/pkg/kube/krt/filter.go @@ -50,14 +50,18 @@ func getKeyExtractor(o any) []string { return []string{GetKey(o)} } -func (f *filter) reverseIndexKey() (string, objectKeyExtractor, bool) { - if f.keys.Len() == 1 { - return f.keys.List()[0], getKeyExtractor, true +// reverseIndexKey +func (f *filter) reverseIndexKey() ([]string, indexedDependencyType, objectKeyExtractor, bool) { + if f.keys.Len() > 0 { + if f.index != nil { + panic("cannot filter by index and key") + } + return f.keys.List(), getKeyType, getKeyExtractor, true } if f.index != nil { - return f.index.key, f.index.extractKeys, true + return []string{f.index.key}, indexType, f.index.extractKeys, true } - return "", nil, false + return nil, unknownIndexType, nil, false } func (f *filter) String() string { diff --git a/pkg/kube/krt/index_test.go b/pkg/kube/krt/index_test.go index 6d5ad4f7f0..322513d78a 100644 --- a/pkg/kube/krt/index_test.go +++ b/pkg/kube/krt/index_test.go @@ -220,3 +220,76 @@ func TestIndexAsCollection(t *testing.T) { assertion("1.2.3.4", 0) assertion("1.2.3.5", 2) } + +type PodCounts struct { + ByIP int + ByName int +} + +func (p PodCounts) ResourceName() string { + return "singleton" +} + +func TestReverseIndex(t *testing.T) { + stop := test.NewStop(t) + opts := testOptions(t) + c := kube.NewFakeClient() + kpc := kclient.New[*corev1.Pod](c) + pc := clienttest.Wrap(t, kpc) + pods := krt.WrapClient[*corev1.Pod](kpc, opts.WithName("Pods")...) + c.RunAndWait(stop) + SimplePods := SimplePodCollection(pods, opts) + tt := assert.NewTracker[string](t) + IPIndex := krt.NewIndex[string, SimplePod](SimplePods, func(o SimplePod) []string { + return []string{o.IP} + }) + Collection := krt.NewSingleton(func(ctx krt.HandlerContext) *PodCounts { + idxPods := krt.Fetch(ctx, SimplePods, krt.FilterIndex(IPIndex, "1.2.3.5")) + namePods := krt.Fetch(ctx, SimplePods, krt.FilterKeys("namespace/name", "namespace/name3")) + return &PodCounts{ + ByIP: len(idxPods), + ByName: len(namePods), + } + }, opts.WithName("Collection")...) + Collection.AsCollection().WaitUntilSynced(stop) + + SimplePods.Register(TrackerHandler[SimplePod](tt)) + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + }, + Status: corev1.PodStatus{PodIP: "1.2.3.4"}, + } + pc.CreateOrUpdateStatus(pod) + assert.EventuallyEqual(t, Collection.Get, &PodCounts{ByIP: 0, ByName: 1}) + + pod.Status.PodIP = "1.2.3.5" + pc.UpdateStatus(pod) + assert.EventuallyEqual(t, Collection.Get, &PodCounts{ByIP: 1, ByName: 1}) + + pod2 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name2", + Namespace: "namespace", + }, + Status: corev1.PodStatus{PodIP: "1.2.3.5"}, + } + pc.CreateOrUpdateStatus(pod2) + assert.EventuallyEqual(t, Collection.Get, &PodCounts{ByIP: 2, ByName: 1}) + + pod3 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name3", + Namespace: "namespace", + }, + Status: corev1.PodStatus{PodIP: "1.2.3.7"}, + } + pc.CreateOrUpdateStatus(pod3) + assert.EventuallyEqual(t, Collection.Get, &PodCounts{ByIP: 2, ByName: 2}) + + pc.Delete(pod.Name, pod.Namespace) + pc.Delete(pod2.Name, pod2.Namespace) + assert.EventuallyEqual(t, Collection.Get, &PodCounts{ByIP: 0, ByName: 1}) +} diff --git a/pkg/kube/krt/internal.go b/pkg/kube/krt/internal.go index e62fd8c356..3fe7d1bf61 100644 --- a/pkg/kube/krt/internal.go +++ b/pkg/kube/krt/internal.go @@ -76,6 +76,7 @@ type collectionOptions struct { type indexedDependency struct { id collectionUID key string + typ indexedDependencyType } // dependency is a specific thing that can be depended on diff --git a/pkg/slices/slices.go b/pkg/slices/slices.go index eb6be069b6..7d51e5dca5 100644 --- a/pkg/slices/slices.go +++ b/pkg/slices/slices.go @@ -125,6 +125,27 @@ func Max[S ~[]E, E cmp.Ordered](x S) E { return slices.Max(x) } +// MaxFunc returns the maximal value in x, using cmp to compare elements. +// It panics if x is empty. If there is more than one maximal element +// according to the cmp function, MaxFunc returns the first one. +func MaxFunc[S ~[]E, E any](x S, cmp func(a, b E) int) E { + return slices.MaxFunc(x, cmp) +} + +// Min returns the minimal value in x. It panics if x is empty. +// For floating-point numbers, Min propagates NaNs (any NaN value in x +// forces the output to be NaN). +func Min[S ~[]E, E cmp.Ordered](x S) E { + return slices.Min(x) +} + +// MinFunc returns the minimal value in x, using cmp to compare elements. +// It panics if x is empty. If there is more than one minimal element +// according to the cmp function, MinFunc returns the first one. +func MinFunc[S ~[]E, E any](x S, cmp func(a, b E) int) E { + return slices.MinFunc(x, cmp) +} + // FindFunc finds the first element matching the function, or nil if none do func FindFunc[E any](s []E, f func(E) bool) *E { idx := slices.IndexFunc(s, f) diff --git a/releasenotes/notes/gateway-customization.yaml b/releasenotes/notes/gateway-customization.yaml new file mode 100644 index 0000000000..e3961abc53 --- /dev/null +++ b/releasenotes/notes/gateway-customization.yaml @@ -0,0 +1,13 @@ +apiVersion: release-notes/v2 +kind: feature +area: traffic-management +issues: + - 53964 + - 46594 + - 53473 + - 54453 +releaseNotes: + - | + **Added** support for customizations to [Gateway API Automated Deployments](https://istio.io/latest/docs/tasks/traffic-management/ingress/gateway-api/#automated-deployment). + This includes both `istio` Gateway types (used for ingress and egress) as well as `istio-waypoint` Gateway types used for ambient mode waypoints. + Users can now customize arbitrary elements of the generated Service, Deployment, ServiceAccount, HorizontalPodAutoscaler, and PodDisruptionBudget.