diff --git a/api/config/v1alpha1/validation/validate.go b/api/config/v1alpha1/validation/validate.go index 14c0f6816ec..6a221376aa3 100644 --- a/api/config/v1alpha1/validation/validate.go +++ b/api/config/v1alpha1/validation/validate.go @@ -56,6 +56,12 @@ func validateEnvoyProxySpec(spec *egcfgv1a1.EnvoyProxySpec) error { errs = append(errs, err) } } + + validateProxyTelemetryErrs := validateProxyTelemetry(spec) + if len(validateProxyTelemetryErrs) != 0 { + errs = append(errs, validateProxyTelemetryErrs...) + } + return utilerrors.NewAggregate(errs) } @@ -156,3 +162,56 @@ func validateBootstrap(boostrapConfig *egcfgv1a1.ProxyBootstrap) error { return nil } + +func validateProxyTelemetry(spec *egcfgv1a1.EnvoyProxySpec) []error { + var errs []error + + if spec != nil && spec.Telemetry.AccessLog != nil { + accessLogErrs := validateProxyAccessLog(spec.Telemetry.AccessLog) + if len(accessLogErrs) > 0 { + errs = append(errs, accessLogErrs...) + } + } + + return errs +} + +func validateProxyAccessLog(accessLog *egcfgv1a1.ProxyAccessLog) []error { + if accessLog.Disable { + return nil + } + + var errs []error + + for _, setting := range accessLog.Settings { + switch setting.Format.Type { + case egcfgv1a1.ProxyAccessLogFormatTypeText: + if setting.Format.Text == nil { + err := fmt.Errorf("unable to configure access log when using Text format but \"text\" field being empty") + errs = append(errs, err) + } + case egcfgv1a1.ProxyAccessLogFormatTypeJSON: + if setting.Format.JSON == nil { + err := fmt.Errorf("unable to configure access log when using JSON format but \"json\" field being empty") + errs = append(errs, err) + } + } + + for _, sink := range setting.Sinks { + switch sink.Type { + case egcfgv1a1.ProxyAccessLogSinkTypeFile: + if sink.File == nil { + err := fmt.Errorf("unable to configure access log when using File sink type but \"file\" field being empty") + errs = append(errs, err) + } + case egcfgv1a1.ProxyAccessLogSinkTypeOpenTelemetry: + if sink.OpenTelemetry == nil { + err := fmt.Errorf("unable to configure access log when using OpenTelemetry sink type but \"openTelemetry\" field being empty") + errs = append(errs, err) + } + } + } + } + + return errs +} diff --git a/api/config/v1alpha1/validation/validate_test.go b/api/config/v1alpha1/validation/validate_test.go index 2fb0a3d1987..754a8911d17 100644 --- a/api/config/v1alpha1/validation/validate_test.go +++ b/api/config/v1alpha1/validation/validate_test.go @@ -246,6 +246,58 @@ func TestValidateEnvoyProxy(t *testing.T) { }, expected: false, }, + { + name: "should invalid when accesslog enabled using Text format, but `text` field being empty", + proxy: &egcfgv1a1.EnvoyProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egcfgv1a1.EnvoyProxySpec{ + Telemetry: egcfgv1a1.ProxyTelemetry{ + AccessLog: &egcfgv1a1.ProxyAccessLog{ + Settings: []egcfgv1a1.ProxyAccessLogSetting{ + { + Format: egcfgv1a1.ProxyAccessLogFormat{ + Type: egcfgv1a1.ProxyAccessLogFormatTypeText, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + { + name: "should invalid when accesslog enabled using File sink, but `file` field being empty", + proxy: &egcfgv1a1.EnvoyProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egcfgv1a1.EnvoyProxySpec{ + Telemetry: egcfgv1a1.ProxyTelemetry{ + AccessLog: &egcfgv1a1.ProxyAccessLog{ + Settings: []egcfgv1a1.ProxyAccessLogSetting{ + { + Format: egcfgv1a1.ProxyAccessLogFormat{ + Type: egcfgv1a1.ProxyAccessLogFormatTypeText, + Text: pointer.String("[%START_TIME%]"), + }, + Sinks: []egcfgv1a1.ProxyAccessLogSink{ + { + Type: egcfgv1a1.ProxyAccessLogSinkTypeFile, + }, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, } for i := range testCases { diff --git a/internal/gatewayapi/listener.go b/internal/gatewayapi/listener.go index b6cf8f372cf..4ba09cbdb4c 100644 --- a/internal/gatewayapi/listener.go +++ b/internal/gatewayapi/listener.go @@ -175,6 +175,10 @@ func processAccessLog(envoyproxy *configv1a1.EnvoyProxy) *ir.AccessLog { for _, sink := range accessLog.Sinks { switch sink.Type { case configv1a1.ProxyAccessLogSinkTypeFile: + if sink.File == nil { + continue + } + switch accessLog.Format.Type { case configv1a1.ProxyAccessLogFormatTypeText: al := &ir.TextAccessLog{ diff --git a/internal/gatewayapi/testdata/envoyproxy-accesslog-file-json-no-format.in.yaml b/internal/gatewayapi/testdata/envoyproxy-accesslog-file-json-no-format.in.yaml new file mode 100644 index 00000000000..658fbbdb41e --- /dev/null +++ b/internal/gatewayapi/testdata/envoyproxy-accesslog-file-json-no-format.in.yaml @@ -0,0 +1,79 @@ +envoyproxy: + apiVersion: config.gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + namespace: envoy-gateway-system + name: test + spec: + telemetry: + accessLog: + settings: + - format: + type: JSON + sinks: + - type: File + file: + path: /dev/stdout + provider: + type: Kubernetes + kubernetes: + envoyService: + type: LoadBalancer + envoyDeployment: + replicas: 2 + container: + env: + - name: env_a + value: env_a_value + - name: env_b + value: env_b_name + image: "envoyproxy/envoy-dev:latest" + resources: + requests: + cpu: 100m + memory: 512Mi + securityContext: + runAsUser: 2000 + allowPrivilegeEscalation: false + pod: + annotations: + key1: val1 + key2: val2 + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: cloud.google.com/gke-nodepool + operator: In + values: + - router-node + tolerations: + - effect: NoSchedule + key: node-type + operator: Exists + value: "router" + securityContext: + runAsUser: 1000 + runAsGroup: 3000 + fsGroup: 2000 + fsGroupChangePolicy: "OnRootMismatch" + volumes: + - name: certs + secret: + secretName: envoy-cert +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: Same diff --git a/internal/gatewayapi/testdata/envoyproxy-accesslog-file-json-no-format.out.yaml b/internal/gatewayapi/testdata/envoyproxy-accesslog-file-json-no-format.out.yaml new file mode 100644 index 00000000000..8daeb7a62d9 --- /dev/null +++ b/internal/gatewayapi/testdata/envoyproxy-accesslog-file-json-no-format.out.yaml @@ -0,0 +1,128 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: Same + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +infraIR: + envoy-gateway/gateway-1: + proxy: + config: + apiVersion: config.gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + creationTimestamp: null + name: test + namespace: envoy-gateway-system + spec: + logging: {} + provider: + kubernetes: + envoyDeployment: + container: + env: + - name: env_a + value: env_a_value + - name: env_b + value: env_b_name + image: envoyproxy/envoy-dev:latest + resources: + requests: + cpu: 100m + memory: 512Mi + securityContext: + allowPrivilegeEscalation: false + runAsUser: 2000 + pod: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: cloud.google.com/gke-nodepool + operator: In + values: + - router-node + annotations: + key1: val1 + key2: val2 + securityContext: + fsGroup: 2000 + fsGroupChangePolicy: OnRootMismatch + runAsGroup: 3000 + runAsUser: 1000 + tolerations: + - effect: NoSchedule + key: node-type + operator: Exists + value: router + volumes: + - name: certs + secret: + secretName: envoy-cert + replicas: 2 + envoyService: + type: LoadBalancer + type: Kubernetes + telemetry: + accessLog: + settings: + - format: + type: JSON + sinks: + - file: + path: /dev/stdout + type: File + status: {} + listeners: + - address: "" + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 +xdsIR: + envoy-gateway/gateway-1: + accessLog: {} + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-1/http + port: 10080 diff --git a/internal/gatewayapi/testdata/envoyproxy-accesslog-file-json.in.yaml b/internal/gatewayapi/testdata/envoyproxy-accesslog-file-json.in.yaml new file mode 100644 index 00000000000..e4ea4939974 --- /dev/null +++ b/internal/gatewayapi/testdata/envoyproxy-accesslog-file-json.in.yaml @@ -0,0 +1,82 @@ +envoyproxy: + apiVersion: config.gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + namespace: envoy-gateway-system + name: test + spec: + telemetry: + accessLog: + settings: + - format: + type: JSON + json: + protocol: "%PROTOCOL%" + duration: "%DURATION%" + sinks: + - type: File + file: + path: /dev/stdout + provider: + type: Kubernetes + kubernetes: + envoyService: + type: LoadBalancer + envoyDeployment: + replicas: 2 + container: + env: + - name: env_a + value: env_a_value + - name: env_b + value: env_b_name + image: "envoyproxy/envoy-dev:latest" + resources: + requests: + cpu: 100m + memory: 512Mi + securityContext: + runAsUser: 2000 + allowPrivilegeEscalation: false + pod: + annotations: + key1: val1 + key2: val2 + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: cloud.google.com/gke-nodepool + operator: In + values: + - router-node + tolerations: + - effect: NoSchedule + key: node-type + operator: Exists + value: "router" + securityContext: + runAsUser: 1000 + runAsGroup: 3000 + fsGroup: 2000 + fsGroupChangePolicy: "OnRootMismatch" + volumes: + - name: certs + secret: + secretName: envoy-cert +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: Same diff --git a/internal/gatewayapi/testdata/envoyproxy-accesslog-file-json.out.yaml b/internal/gatewayapi/testdata/envoyproxy-accesslog-file-json.out.yaml new file mode 100644 index 00000000000..aeaa5183ef6 --- /dev/null +++ b/internal/gatewayapi/testdata/envoyproxy-accesslog-file-json.out.yaml @@ -0,0 +1,136 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: Same + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +infraIR: + envoy-gateway/gateway-1: + proxy: + config: + apiVersion: config.gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + creationTimestamp: null + name: test + namespace: envoy-gateway-system + spec: + logging: {} + provider: + kubernetes: + envoyDeployment: + container: + env: + - name: env_a + value: env_a_value + - name: env_b + value: env_b_name + image: envoyproxy/envoy-dev:latest + resources: + requests: + cpu: 100m + memory: 512Mi + securityContext: + allowPrivilegeEscalation: false + runAsUser: 2000 + pod: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: cloud.google.com/gke-nodepool + operator: In + values: + - router-node + annotations: + key1: val1 + key2: val2 + securityContext: + fsGroup: 2000 + fsGroupChangePolicy: OnRootMismatch + runAsGroup: 3000 + runAsUser: 1000 + tolerations: + - effect: NoSchedule + key: node-type + operator: Exists + value: router + volumes: + - name: certs + secret: + secretName: envoy-cert + replicas: 2 + envoyService: + type: LoadBalancer + type: Kubernetes + telemetry: + accessLog: + settings: + - format: + json: + duration: '%DURATION%' + protocol: '%PROTOCOL%' + type: JSON + sinks: + - file: + path: /dev/stdout + type: File + status: {} + listeners: + - address: "" + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 +xdsIR: + envoy-gateway/gateway-1: + accessLog: + json: + - json: + duration: '%DURATION%' + protocol: '%PROTOCOL%' + path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-1/http + port: 10080 diff --git a/internal/gatewayapi/testdata/envoyproxy-accesslog-with-bad-sinks.in.yaml b/internal/gatewayapi/testdata/envoyproxy-accesslog-with-bad-sinks.in.yaml new file mode 100644 index 00000000000..ce2ac6eb467 --- /dev/null +++ b/internal/gatewayapi/testdata/envoyproxy-accesslog-with-bad-sinks.in.yaml @@ -0,0 +1,80 @@ +envoyproxy: + apiVersion: config.gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + namespace: envoy-gateway-system + name: test + spec: + telemetry: + accessLog: + settings: + - format: + type: Text + text: | + [%START_TIME%] "%REQ(:METHOD)% %PROTOCOL%" %RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-FORWARDED-FOR)%" "%REQ(USER-AGENT)%" "%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%"\n + sinks: + - type: File + - type: OpenTelemetry + provider: + type: Kubernetes + kubernetes: + envoyService: + type: LoadBalancer + envoyDeployment: + replicas: 2 + container: + env: + - name: env_a + value: env_a_value + - name: env_b + value: env_b_name + image: "envoyproxy/envoy-dev:latest" + resources: + requests: + cpu: 100m + memory: 512Mi + securityContext: + runAsUser: 2000 + allowPrivilegeEscalation: false + pod: + annotations: + key1: val1 + key2: val2 + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: cloud.google.com/gke-nodepool + operator: In + values: + - router-node + tolerations: + - effect: NoSchedule + key: node-type + operator: Exists + value: "router" + securityContext: + runAsUser: 1000 + runAsGroup: 3000 + fsGroup: 2000 + fsGroupChangePolicy: "OnRootMismatch" + volumes: + - name: certs + secret: + secretName: envoy-cert +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: Same diff --git a/internal/gatewayapi/testdata/envoyproxy-accesslog-with-bad-sinks.out.yaml b/internal/gatewayapi/testdata/envoyproxy-accesslog-with-bad-sinks.out.yaml new file mode 100644 index 00000000000..ed63dfbeff1 --- /dev/null +++ b/internal/gatewayapi/testdata/envoyproxy-accesslog-with-bad-sinks.out.yaml @@ -0,0 +1,129 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: Same + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +infraIR: + envoy-gateway/gateway-1: + proxy: + config: + apiVersion: config.gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + creationTimestamp: null + name: test + namespace: envoy-gateway-system + spec: + logging: {} + provider: + kubernetes: + envoyDeployment: + container: + env: + - name: env_a + value: env_a_value + - name: env_b + value: env_b_name + image: envoyproxy/envoy-dev:latest + resources: + requests: + cpu: 100m + memory: 512Mi + securityContext: + allowPrivilegeEscalation: false + runAsUser: 2000 + pod: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: cloud.google.com/gke-nodepool + operator: In + values: + - router-node + annotations: + key1: val1 + key2: val2 + securityContext: + fsGroup: 2000 + fsGroupChangePolicy: OnRootMismatch + runAsGroup: 3000 + runAsUser: 1000 + tolerations: + - effect: NoSchedule + key: node-type + operator: Exists + value: router + volumes: + - name: certs + secret: + secretName: envoy-cert + replicas: 2 + envoyService: + type: LoadBalancer + type: Kubernetes + telemetry: + accessLog: + settings: + - format: + text: | + [%START_TIME%] "%REQ(:METHOD)% %PROTOCOL%" %RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-FORWARDED-FOR)%" "%REQ(USER-AGENT)%" "%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%"\n + type: Text + sinks: + - type: File + - type: OpenTelemetry + status: {} + listeners: + - address: "" + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 +xdsIR: + envoy-gateway/gateway-1: + accessLog: {} + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-1/http + port: 10080