diff --git a/api/v1alpha1/frontendenvironment_types.go b/api/v1alpha1/frontendenvironment_types.go index 534e928d..dfc034dc 100644 --- a/api/v1alpha1/frontendenvironment_types.go +++ b/api/v1alpha1/frontendenvironment_types.go @@ -100,6 +100,9 @@ type FrontendEnvironmentSpec struct { TargetNamespaces []string `json:"targetNamespaces,omitempty" yaml:"targetNamespaces,omitempty"` // For the ChromeUI to render additional global components ServiceCategories *[]FrontendServiceCategory `json:"serviceCategories,omitempty" yaml:"serviceCategories,omitempty"` + // Custom HTTP Headers + // These populate an ENV var that is then added into the caddy config as a header block + HTTPHeaders map[string]string `json:"httpHeaders,omitempty"` DefaultReplicas *int32 `json:"defaultReplicas,omitempty" yaml:"defaultReplicas,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 839ce7d4..cf9e3ee5 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -501,6 +501,13 @@ func (in *FrontendEnvironmentSpec) DeepCopyInto(out *FrontendEnvironmentSpec) { } } } + if in.HTTPHeaders != nil { + in, out := &in.HTTPHeaders, &out.HTTPHeaders + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } if in.DefaultReplicas != nil { in, out := &in.DefaultReplicas, &out.DefaultReplicas *out = new(int32) diff --git a/config/crd/bases/cloud.redhat.com_frontendenvironments.yaml b/config/crd/bases/cloud.redhat.com_frontendenvironments.yaml index 1db30057..193019a8 100644 --- a/config/crd/bases/cloud.redhat.com_frontendenvironments.yaml +++ b/config/crd/bases/cloud.redhat.com_frontendenvironments.yaml @@ -82,6 +82,13 @@ spec: hostname: description: Hostname type: string + httpHeaders: + additionalProperties: + type: string + description: |- + Custom HTTP Headers + These populate an ENV var that is then added into the caddy config as a header block + type: object ingressAnnotations: additionalProperties: type: string diff --git a/controllers/reconcile.go b/controllers/reconcile.go index 814758a5..53be5957 100644 --- a/controllers/reconcile.go +++ b/controllers/reconcile.go @@ -397,23 +397,54 @@ func populateVolumes(d *apps.Deployment, frontend *crd.Frontend, frontendEnviron d.Spec.Template.Spec.Volumes = volumes } -// Add the SSL env vars if we SSL mode is set in the frontend environment +// Add the env vars if eny are set func (r *FrontendReconciliation) populateEnvVars(d *apps.Deployment, frontendEnvironment *crd.FrontendEnvironment) { - if !frontendEnvironment.Spec.SSL { - return - } - envVars := []v1.EnvVar{ - { + envVars := []v1.EnvVar{} + + if frontendEnvironment.Spec.SSL { + envVars = append(envVars, v1.EnvVar{ Name: "CADDY_TLS_MODE", Value: "https_port 8000", - }, - { + }) + envVars = append(envVars, v1.EnvVar{ Name: "CADDY_TLS_CERT", Value: "tls /opt/certs/tls.crt /opt/certs/tls.key", - }} + }) + } + + // Add the HTTP Headers to the env vars if the frontend environment has them + if frontendEnvironment.Spec.HTTPHeaders != nil { + envVars = append(envVars, v1.EnvVar{ + Name: "CADDY_HTTP_HEADERS", + Value: createCaddyfileHeaderBlock(frontendEnvironment.Spec.HTTPHeaders), + }) + } + d.Spec.Template.Spec.Containers[0].Env = envVars } +// Create a Caddyfile header block from the FrontenbdEnvironment httpHeaders +func createCaddyfileHeaderBlock(httpHeaders map[string]string) string { + if len(httpHeaders) == 0 { + return "" + } + var keys []string + for key := range httpHeaders { + keys = append(keys, key) + } + // Sort keys alphabetically + // We do this to ensure the order of the headers is consistent + // without this tests will be flaky + sort.Strings(keys) + + headerBlock := "header {\n" + for _, key := range keys { + headerBlock += fmt.Sprintf("%s %s\n", key, httpHeaders[key]) + } + headerBlock += "}" + return headerBlock +} + func (r *FrontendReconciliation) createFrontendDeployment(annotationHashes []map[string]string) error { // Create new empty struct diff --git a/deploy.yml b/deploy.yml index e283c179..f0a25363 100644 --- a/deploy.yml +++ b/deploy.yml @@ -286,6 +286,14 @@ objects: hostname: description: Hostname type: string + httpHeaders: + additionalProperties: + type: string + description: 'Custom HTTP Headers + + These populate an ENV var that is then added into the caddy config + as a header block' + type: object ingressAnnotations: additionalProperties: type: string diff --git a/docs/antora/modules/ROOT/pages/api_reference.adoc b/docs/antora/modules/ROOT/pages/api_reference.adoc index 1cd0136d..8ec8fd46 100644 --- a/docs/antora/modules/ROOT/pages/api_reference.adoc +++ b/docs/antora/modules/ROOT/pages/api_reference.adoc @@ -465,6 +465,8 @@ Preserving for backwards compatibility + | | | *`targetNamespaces`* __string array__ | List of namespaces that should receive a copy of the frontend configuration as a config map + By configurations we mean the fed-modules.json, navigation files, etc. + | | | *`serviceCategories`* __xref:{anchor_prefix}-github-com-redhatinsights-frontend-operator-api-v1alpha1-frontendservicecategory[$$FrontendServiceCategory$$]__ | For the ChromeUI to render additional global components + | | +| *`httpHeaders`* __object (keys:string, values:string)__ | Custom HTTP Headers + +These populate an ENV var that is then added into the caddy config as a header block + | | | *`defaultReplicas`* __integer__ | | | |=== diff --git a/tests/e2e/http_headers/00-create-namespace.yaml b/tests/e2e/http_headers/00-create-namespace.yaml new file mode 100644 index 00000000..ea4fe821 --- /dev/null +++ b/tests/e2e/http_headers/00-create-namespace.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test-http-headers +spec: + finalizers: + - kubernetes diff --git a/tests/e2e/http_headers/01-create-resources.yaml b/tests/e2e/http_headers/01-create-resources.yaml new file mode 100644 index 00000000..35d0b5aa --- /dev/null +++ b/tests/e2e/http_headers/01-create-resources.yaml @@ -0,0 +1,38 @@ +--- +apiVersion: cloud.redhat.com/v1alpha1 +kind: FrontendEnvironment +metadata: + name: test-http-headers-environment +spec: + generateNavJSON: false + ssl: false + hostname: foo.redhat.com + sso: https://sso.foo.redhat.com + httpHeaders: + "X-Frame-Options": "Set" + "X-XSS-Protection": "1; mode=block;" + "X-Content-Type-Options": "nosniff" + "Content-Security-Policy": "default-src 'self'" + "Referrer-Policy": "no-referrer" +--- +apiVersion: cloud.redhat.com/v1alpha1 +kind: Frontend +metadata: + name: chrome + namespace: test-http-headers +spec: + API: + versions: + - v1 + frontend: + paths: + - / + deploymentRepo: https://github.com/RedHatInsights/insights-chrome + envName: test-http-headers-environment + image: quay.io/cloudservices/insights-chrome-frontend:720317c + module: + config: + ssoUrl: 'https://' + manifestLocation: /apps/chrome/js/fed-mods.json + title: Chrome + diff --git a/tests/e2e/http_headers/02-assert.yaml b/tests/e2e/http_headers/02-assert.yaml new file mode 100644 index 00000000..0b4365b4 --- /dev/null +++ b/tests/e2e/http_headers/02-assert.yaml @@ -0,0 +1,46 @@ +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: chrome-frontend + namespace: test-http-headers + labels: + frontend: chrome + ownerReferences: + - apiVersion: cloud.redhat.com/v1alpha1 + kind: Frontend + name: chrome +spec: + selector: + matchLabels: + frontend: chrome + template: + spec: + volumes: + - name: config + configMap: + name: test-http-headers-environment + defaultMode: 420 + containers: + - name: fe-image + image: 'quay.io/cloudservices/insights-chrome-frontend:720317c' + env: + - name: CADDY_HTTP_HEADERS + value: |- + header { + Content-Security-Policy default-src 'self' + Referrer-Policy no-referrer + X-Content-Type-Options nosniff + X-Frame-Options Set + X-XSS-Protection 1; mode=block; + } + ports: + - name: web + containerPort: 80 + protocol: TCP + - name: metrics + containerPort: 9000 + protocol: TCP + volumeMounts: + - name: config + mountPath: /opt/app-root/src/build/stable/operator-generated \ No newline at end of file