Skip to content

Commit

Permalink
fix: creating a robot account for each project and adding imagepullse… (
Browse files Browse the repository at this point in the history
#34)

Creating imagePullSecret for team docker image repository
  • Loading branch information
mojtabaimani authored Jun 28, 2021
1 parent 6143dc8 commit 596152e
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 36 deletions.
23 changes: 23 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// cSpell Settings
{
// Version of the setting file. Always 0.1
"version": "0.1",
// language - current active spelling language
"language": "en",
// words - list of words to be always considered correct
"words": [
"camelcase",
"creds",
"kubernetes",
"openid",
"otomi",
"redkubes",
"robotv"
],
// flagWords - list of words to be always considered incorrect
// This is useful for offensive words and common spelling errors.
// For example "hte" should be "the"
"flagWords": [
"hte"
]
}
5 changes: 3 additions & 2 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
HARBOR_USER='admin'
HARBOR_PASSWORD=''
HARBOR_BASE_URL='http://127.0.0.1:8083/api/v2.0'
HARBOR_BASE_REPO_URL='harbor.dev.eks.otomi.cloud'
OIDC_CLIENT_SECRET=''
OIDC_ENDPOINT='https://keycloak.dev.gke.otomi.cloud'
OIDC_VERIFY_CERT='true' # set to false when staging certs are used on the cluster
TEAM_NAMES='["team-otomi","team-chai","team-dev","team-demo"]'
OIDC_VERIFY_CERT='true'
TEAM_IDS='["otomi","chai","dev","demo"]'

# keycloak
TENANT_ID=''
Expand Down
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@
"prettier.disableLanguages": ["javascript", "typescript"],
"prettier.enable": true,
"typescript.autoClosingTags": false,
"standard.enable": false
"standard.enable": false,
}
135 changes: 105 additions & 30 deletions src/tasks/harbor/harbor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,33 @@ import {
ProjectMember,
ProjectReq,
RobotApi,
Robotv1Api,
Project,
RobotCreate,
} from '@redkubes/harbor-client-node'

import {
cleanEnv,
HARBOR_BASE_URL,
HARBOR_BASE_REPO_URL,
HARBOR_PASSWORD,
HARBOR_USER,
OIDC_CLIENT_SECRET,
OIDC_ENDPOINT,
OIDC_VERIFY_CERT,
TEAM_NAMES,
TEAM_IDS,
} from '../../validators'
import { createSecret, ensure, getApiClient, getSecret, doApiCall, handleErrors } from '../../utils'
import { createSecret, getApiClient, getSecret, doApiCall, handleErrors, createPullSecret } from '../../utils'

const env = cleanEnv({
HARBOR_BASE_URL,
HARBOR_BASE_REPO_URL,
HARBOR_PASSWORD,
HARBOR_USER,
OIDC_CLIENT_SECRET,
OIDC_ENDPOINT,
OIDC_VERIFY_CERT,
TEAM_NAMES,
TEAM_IDS,
})

const HarborRole = {
Expand All @@ -52,9 +56,9 @@ export interface RobotSecret {
secret: string
}

const robot: any = {
const systemRobot: any = {
name: 'harbor',
duration: 0,
duration: -1,
description: 'Used by Otomi Harbor task runner',
disable: false,
level: 'system',
Expand Down Expand Up @@ -85,10 +89,13 @@ const config: any = {
self_registration: false,
}

const namespace = 'harbor'
const secretName = 'harbor-robot-admin'
const systemNamespace = 'harbor'
const systemSecretName = 'harbor-robot-admin'
const projectSecretName = 'image-pull-secret'
const projectRobotName = 'kubernetes'
const bearerAuth: HttpBearerAuth = new HttpBearerAuth()
const robotApi = new RobotApi(env.HARBOR_USER, env.HARBOR_PASSWORD, env.HARBOR_BASE_URL)
const robotv1Api = new Robotv1Api(env.HARBOR_USER, env.HARBOR_PASSWORD, env.HARBOR_BASE_URL)
const configureApi = new ConfigureApi(env.HARBOR_USER, env.HARBOR_PASSWORD, env.HARBOR_BASE_URL)
const projectsApi = new ProjectApi(env.HARBOR_USER, env.HARBOR_PASSWORD, env.HARBOR_BASE_URL)
const memberApi = new MemberApi(env.HARBOR_USER, env.HARBOR_PASSWORD, env.HARBOR_BASE_URL)
Expand All @@ -99,28 +106,72 @@ function setAuth(secret): void {
}

// NOTE: assumes OIDC is not yet configured, otherwise this operation is NOT possible
async function createRobotSecret(): Promise<RobotSecret> {
async function createSystemRobotSecret(): Promise<RobotSecret> {
const { body: robotList } = await robotApi.listRobot()
const existing = robotList.find((i) => i.name === `robot$${robot.name}`)
const existing = robotList.find((i) => i.name === `robot$${systemRobot.name}`)
if (existing?.id) {
const existingId = existing.id
await doApiCall(errors, `Deleting previous robot account ${robot.name}`, () => robotApi.deleteRobot(existingId))
await doApiCall(errors, `Deleting previous robot account ${systemRobot.name}`, () =>
robotApi.deleteRobot(existingId),
)
}
const { id, name, secret } = await doApiCall(
errors,
`Create robot account ${robot.name} with system level perms`,
() => robotApi.createRobot(robot),
`Create robot account ${systemRobot.name} with system level perms`,
() => robotApi.createRobot(systemRobot),
)
const robotSecret: RobotSecret = { id, name, secret }
await createSecret(secretName, namespace, robotSecret)
await createSecret(systemSecretName, systemNamespace, robotSecret)
return robotSecret
}

async function ensureSecret(): Promise<RobotSecret> {
let robotSecret = (await getSecret(secretName, namespace)) as RobotSecret
async function createProjectRobotSecret(teamId: string, projectId: string): Promise<RobotSecret> {
const namespace = `team-${teamId}`
const projectRobot: RobotCreate = {
name: projectRobotName,
duration: -1,
description: 'Used by kubernetes to pull images from harbor in each team',
disable: false,
level: 'project',
permissions: [
{
kind: 'project',
namespace,
access: [
{
resource: 'repository',
action: 'pull',
},
],
},
],
}

const { body: robotList } = await robotv1Api.listRobotV1(projectId)
const existing = robotList.find((i) => i.name === `robot$${namespace}+${projectRobot.name}`)

if (existing?.id) {
const existingId = existing.id
await doApiCall(errors, `Deleting previous robot account ${existing.name}`, () =>
robotv1Api.deleteRobotV1(projectId, existingId),
)
}

const { id, name, secret } = await doApiCall(
errors,
`Create project robot account ${projectRobot.name} with project level perms`,
// () => robotv1Api.createRobotV1(projectId, projectRobot), // this function didn't work. I couldn't fix the expiration time with this function. I have to use the robotApi
() => robotApi.createRobot(projectRobot),
)
const robotSecret: RobotSecret = { id, name, secret }
return robotSecret
}

async function ensureSystemSecret(): Promise<RobotSecret> {
let robotSecret = (await getSecret(systemSecretName, systemNamespace)) as RobotSecret
if (!robotSecret) {
// not existing yet, create robot account and keep creds in secret
robotSecret = await createRobotSecret()
robotSecret = await createSystemRobotSecret()
} else {
// test if secret still works
try {
Expand All @@ -130,44 +181,64 @@ async function ensureSecret(): Promise<RobotSecret> {
// throw everything expect 401, which is what we test for
if (e.status !== 401) throw e
// unauthenticated, so remove and recreate secret
await getApiClient().deleteNamespacedSecret(secretName, namespace)
await getApiClient().deleteNamespacedSecret(systemSecretName, systemNamespace)
// now, the next call might throw IF:
// - authMode oidc was already turned on and an otomi admin accidentally removed the secret
// but that is very unlikely, an unresolvable problem and needs a manual db fix
robotSecret = await createRobotSecret()
robotSecret = await createSystemRobotSecret()
}
}
setAuth(robotSecret.secret)
return robotSecret
}

async function ensureProjectSecret(teamId: string, projectId: string): Promise<void> {
const namespace = `team-${teamId}`

let k8sSecret = (await getSecret(projectSecretName, namespace)) as RobotSecret
if (k8sSecret) {
await getApiClient().deleteNamespacedSecret(projectSecretName, namespace)
}

k8sSecret = await createProjectRobotSecret(teamId, projectId)
await createPullSecret({
teamId,
name: projectSecretName,
server: `${env.HARBOR_BASE_REPO_URL}`,
username: `robot$${namespace}+${projectRobotName}`,
password: k8sSecret.secret,
})
}

async function main(): Promise<void> {
await ensureSecret()
await ensureSystemSecret()

// now we can set the token on our apis
// too bad we can't set it globally
configureApi.setDefaultAuthentication(bearerAuth)
projectsApi.setDefaultAuthentication(bearerAuth)
memberApi.setDefaultAuthentication(bearerAuth)
robotv1Api.setDefaultAuthentication(bearerAuth)

await doApiCall(errors, 'Putting Harbor configuration', () => configureApi.configurationsPut(config))
await Promise.all(
env.TEAM_NAMES.map(async (team) => {
env.TEAM_IDS.map(async (teamId: string) => {
const namespace = `team-${teamId}`
const projectReq: ProjectReq = {
projectName: team,
projectName: namespace,
}
await doApiCall(errors, `Creating project for team ${team}`, () => projectsApi.createProject(projectReq))
const project = (await doApiCall(errors, `Get project for team ${team}`, () =>
projectsApi.getProject(team),
)) as Project
await doApiCall(errors, `Creating project for team ${teamId}`, () => projectsApi.createProject(projectReq))

if (!project) return
const projectId = `${ensure(project.projectId)}`
const project = (await doApiCall(errors, `Get project for team ${teamId}`, () =>
projectsApi.getProject(namespace),
)) as Project
if (!project) return ''
const projectId = `${project.projectId}`

const projMember: ProjectMember = {
roleId: HarborRole.developer,
memberGroup: {
groupName: team,
groupName: namespace,
groupType: HarborGroupType.http,
},
}
Expand All @@ -178,12 +249,16 @@ async function main(): Promise<void> {
groupType: HarborGroupType.http,
},
}
await doApiCall(errors, `Associating "developer" role for team "${team}" with harbor project "${team}"`, () =>
await doApiCall(errors, `Associating "developer" role for team "${teamId}" with harbor project "${teamId}"`, () =>
memberApi.createProjectMember(projectId, undefined, undefined, projMember),
)
await doApiCall(errors, `Associating "project-admin" role for "team-admin" with harbor project "${team}"`, () =>
await doApiCall(errors, `Associating "project-admin" role for "team-admin" with harbor project "${teamId}"`, () =>
memberApi.createProjectMember(projectId, undefined, undefined, projAdminMember),
)

await ensureProjectSecret(teamId, projectId)

return null
}),
)

Expand Down
4 changes: 2 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,15 +126,15 @@ export async function createPullSecret({
username,
password,
email: 'not@val.id',
auth: username + Buffer.from(password).toString('base64'),
auth: Buffer.from(`${username}:${password}`).toString('base64'),
},
},
}
// create the secret
const secret = {
...new V1Secret(),
metadata: { ...new V1ObjectMeta(), name },
type: 'docker-registry',
type: 'kubernetes.io/dockerconfigjson',
data: {
'.dockerconfigjson': Buffer.from(JSON.stringify(data)).toString('base64'),
},
Expand Down
3 changes: 2 additions & 1 deletion src/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { str, bool, json, cleanEnv as clean, CleanEnv, StrictCleanOptions, Valid
export const CERT_ROTATION_DAYS = num({ desc: 'The amount of days for the cert rotation', default: 75 })
export const DOMAINS = json({ desc: 'A list of domains and their cert status' })
export const HARBOR_BASE_URL = str({ desc: 'The harbor core service URL' })
export const HARBOR_BASE_REPO_URL = str({ desc: 'The harbor repository base URL' })
export const HARBOR_PASSWORD = str({ desc: 'The harbor admin password' })
export const HARBOR_USER = str({ desc: 'The harbor admin username' })
export const IDP_ALIAS = str({ desc: 'An alias for the IDP' })
Expand Down Expand Up @@ -37,7 +38,7 @@ export const OIDC_VERIFY_CERT = bool({ desc: 'Wether to validate the OIDC endpoi
export const REDIRECT_URIS = json({ desc: "A list of redirect URI's in JSON format" })
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_NAMES = json({ desc: 'A list of team names in JSON format' })
export const TEAM_IDS = json({ desc: 'A list of team ids in JSON format' })
export const TENANT_CLIENT_ID = str({ desc: 'The tenant client id' })
export const TENANT_CLIENT_SECRET = str({ desc: 'The tenant client secret' })
export const TENANT_ID = str({ desc: 'The tenant ID' })
Expand Down

0 comments on commit 596152e

Please sign in to comment.