Skip to content

Commit

Permalink
feat: host headers for internal requests (#67)
Browse files Browse the repository at this point in the history
to set host headers on keycloak backchannel requests
  • Loading branch information
Maurice Faber committed Apr 7, 2022
1 parent 75af68a commit 7d9c61a
Show file tree
Hide file tree
Showing 7 changed files with 25 additions and 17 deletions.
4 changes: 2 additions & 2 deletions src/tasks/gitea/gitea.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { OrganizationApi, CreateRepoOption, CreateOrgOption, CreateTeamOption } from '@redkubes/gitea-client-node'
import { CreateOrgOption, CreateRepoOption, CreateTeamOption, OrganizationApi } from '@redkubes/gitea-client-node'
import { doApiCall, waitTillAvailable } from '../../utils'
import { cleanEnv, GITEA_PASSWORD, GITEA_URL } from '../../validators'
import { orgName, repoName, username, teamNameViewer } from '../common'
import { orgName, repoName, teamNameViewer, username } from '../common'

const env = cleanEnv({
GITEA_PASSWORD,
Expand Down
2 changes: 1 addition & 1 deletion src/tasks/harbor/harbor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ async function ensureTeamRobotAccountSecret(namespace: string, projectName): Pro

async function main(): Promise<void> {
// harborHealthUrl is an in-cluster http svc, so no multiple external dns confirmations are needed
await waitTillAvailable(harborHealthUrl, { confirmations: 1 })
await waitTillAvailable(harborHealthUrl, undefined, { confirmations: 1 })
const bearerAuth = await getBearerToken()
robotApi.setDefaultAuthentication(bearerAuth)
configureApi.setDefaultAuthentication(bearerAuth)
Expand Down
14 changes: 9 additions & 5 deletions src/tasks/keycloak/keycloak.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ import {
UsersApi,
} from '@redkubes/keycloak-client-node'
import { forEach } from 'lodash'
import { Issuer, TokenSet } from 'openid-client'
import { custom, Issuer, TokenSet } from 'openid-client'
import { doApiCall, handleErrors, waitTillAvailable } from '../../utils'
import {
cleanEnv,
FEAT_EXTERNAL_IDP,
IDP_ALIAS,
IDP_OIDC_URL,
KEYCLOAK_ADDRESS,
KEYCLOAK_ADDRESS_INTERNAL,
KEYCLOAK_ADMIN,
KEYCLOAK_ADMIN_PASSWORD,
KEYCLOAK_REALM,
Expand All @@ -52,19 +53,22 @@ const env = cleanEnv({
KEYCLOAK_ADMIN,
KEYCLOAK_ADMIN_PASSWORD,
KEYCLOAK_ADDRESS,
KEYCLOAK_ADDRESS_INTERNAL,
KEYCLOAK_REALM,
FEAT_EXTERNAL_IDP,
})

const errors: string[] = []

async function main(): Promise<void> {
await waitTillAvailable(env.KEYCLOAK_ADDRESS)
const keycloakAddress = env.KEYCLOAK_ADDRESS
const basePath = `${keycloakAddress}/admin/realms`
await waitTillAvailable(env.KEYCLOAK_ADDRESS_INTERNAL)
const keycloakSvc = env.KEYCLOAK_ADDRESS_INTERNAL || 'http://keycloak-http.keycloak'
const basePath = `${keycloakSvc}/admin/realms`
let token: TokenSet
try {
const keycloakIssuer = await Issuer.discover(`${keycloakAddress}/realms/${env.KEYCLOAK_REALM}/`)
custom.setHttpOptionsDefaults({ headers: { host: env.KEYCLOAK_ADDRESS.replace('https://', '') } })
const keycloakIssuer = await Issuer.discover(`${keycloakSvc}/realms/${env.KEYCLOAK_REALM}/`)
// console.log(keycloakIssuer)
const clientOptions: any = {
client_id: 'admin-cli',
client_secret: 'unused',
Expand Down
6 changes: 3 additions & 3 deletions src/tasks/otomi/wait-for.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { waitTillAvailable } from '../../utils'
import { cleanEnv, WAIT_OPTIONS, WAIT_URL } from '../../validators'
import { cleanEnv, WAIT_OPTIONS, WAIT_URL, WAIT_HOST } from '../../validators'

const env = cleanEnv({ WAIT_OPTIONS, WAIT_URL })
const env = cleanEnv({ WAIT_OPTIONS, WAIT_URL, WAIT_HOST })

if (typeof require !== 'undefined' && require.main === module) {
waitTillAvailable(env.WAIT_URL, env.WAIT_OPTIONS)
waitTillAvailable(env.WAIT_URL, env.WAIT_HOST, env.WAIT_OPTIONS)
}
10 changes: 5 additions & 5 deletions src/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('utils', () => {
it('should pass after x successful requests', async () => {
const stub = sandbox.stub(fetch, 'Promise').returns(successResp)
const confirmations = 3
const res = waitTillAvailable(url, { confirmations })
const res = waitTillAvailable(url, undefined, { confirmations })
await tick(confirmations + 2) // wait extra rounds
expect(stub).to.have.callCount(confirmations)
await expect(res).to.eventually.be.fulfilled
Expand All @@ -55,7 +55,7 @@ describe('utils', () => {
it('should reset confirmation counter', async () => {
const stub = sandbox.stub(fetch, 'Promise').returns(successResp)
const confirmations = 3
const res = waitTillAvailable(url, { confirmations })
const res = waitTillAvailable(url, undefined, { confirmations })
await tick(confirmations + 2) // wait extra rounds
expect(stub).to.have.callCount(confirmations)
await expect(res).to.eventually.be.fulfilled
Expand All @@ -64,7 +64,7 @@ describe('utils', () => {
it('should bail when a request returns an unexpected status code', async () => {
const stub = sandbox.stub(fetch, 'Promise').returns(failResp)
const retries = 3
const res = waitTillAvailable(url, { retries })
const res = waitTillAvailable(url, undefined, { retries })
await tick(retries + 1)
expect(stub).to.have.callCount(1)
await expect(res).to.eventually.be.rejectedWith(`Wrong status code: 500`)
Expand All @@ -74,7 +74,7 @@ describe('utils', () => {
const stub = sandbox.stub(fetch, 'Promise').throws(new Error('ECONNREFUSED'))
const retries = 3
const maxTimeout = 30000
const res = waitTillAvailable(url, { retries, maxTimeout })
const res = waitTillAvailable(url, undefined, { retries, maxTimeout })
await tick(retries + 2, maxTimeout) // run a couple extra rounds and set duration to maxTimeout to make sure we have spent enough time
expect(stub).to.have.callCount(3)
await expect(res).to.eventually.be.rejectedWith(`Max retries (${retries}) has been reached!`)
Expand All @@ -85,7 +85,7 @@ describe('utils', () => {
const confirmations = 3
const retries = 1000 // large enough
const maxTimeout = 1000 // same as minTimeout to be able to calculate attempts
const res = waitTillAvailable(url, { confirmations, retries, maxTimeout, forever: true })
const res = waitTillAvailable(url, undefined, { confirmations, retries, maxTimeout, forever: true })
// tick 5 failures
await tick(5)
// now start returning ok responses
Expand Down
4 changes: 3 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import retry, { Options } from 'async-retry'
import http, { Agent as AgentHttp } from 'http'
import { Agent } from 'https'
import { set } from 'lodash'
import fetch, { RequestInit } from 'node-fetch'
import { cleanEnv, NODE_EXTRA_CA_CERTS, NODE_TLS_REJECT_UNAUTHORIZED } from './validators'

Expand Down Expand Up @@ -76,7 +77,7 @@ const sleep = (ms) => {
return new Promise((resolve) => setTimeout(resolve, ms))
}

export const waitTillAvailable = async (url: string, opts?: WaitTillAvailableOptions): Promise<void> => {
export const waitTillAvailable = async (url: string, host?: string, opts?: WaitTillAvailableOptions): Promise<void> => {
const options: WaitTillAvailableOptions = { ...defaultOptions, ...opts }
if (env.isDev) {
options.confirmations = 1
Expand All @@ -89,6 +90,7 @@ export const waitTillAvailable = async (url: string, opts?: WaitTillAvailableOpt
: new AgentHttp(),
timeout: 5000,
}
if (host) set(fetchOptions, 'headers.host', host)

// we don't trust dns in the cluster and want a lot of confirmations
// but we don't care about those when we call the cluster from outside
Expand Down
2 changes: 2 additions & 0 deletions src/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const DRONE_URL = str({ desc: 'The public url of the drone server' })
export const GITEA_PASSWORD = str({ desc: 'The gitea admin password' })
export const GITEA_URL = url({ desc: 'The gitea core service url' })
export const KEYCLOAK_ADDRESS = str({ desc: 'The Keycloak Server address' })
export const KEYCLOAK_ADDRESS_INTERNAL = str({ desc: 'The internal Keycloak kubernetes svc address' })
export const KEYCLOAK_ADMIN = str({ desc: 'Default admin username for KeyCloak Server', default: 'admin' })
export const KEYCLOAK_ADMIN_PASSWORD = str({ desc: 'Default password for admin' })
export const KEYCLOAK_CLIENT_ID = str({ desc: 'Default Keycloak Client', default: 'otomi' })
Expand All @@ -63,6 +64,7 @@ export const REGION = str({ desc: 'The cloud region' })
export const SECRETS_NAMESPACE = str({ desc: 'The namespace of the TLS secrets', default: 'istio-system' })
export const TEAM_IDS = json({ desc: 'A list of team ids in JSON format' })
export const WAIT_URL = str({ desc: 'The URL to wait for.' })
export const WAIT_HOST = str({ desc: 'The HOST header that goes with the url to wait for.', default: undefined })
export const WAIT_OPTIONS = json({ desc: 'The waitTillAvailable options', default: '{}' })

// set default to undefined based on feature flags:
Expand Down

0 comments on commit 7d9c61a

Please sign in to comment.