From 596152e054599bd858f5e1e0ddc7629a48b26966 Mon Sep 17 00:00:00 2001 From: Mojtaba Imani Date: Mon, 28 Jun 2021 09:26:17 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20creating=20a=20robot=20account=20for=20e?= =?UTF-8?q?ach=20project=20and=20adding=20imagepullse=E2=80=A6=20(#34)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creating imagePullSecret for team docker image repository --- .cspell.json | 23 +++++++ .env.sample | 5 +- .vscode/settings.json | 2 +- src/tasks/harbor/harbor.ts | 135 ++++++++++++++++++++++++++++--------- src/utils.ts | 4 +- src/validators.ts | 3 +- 6 files changed, 136 insertions(+), 36 deletions(-) create mode 100644 .cspell.json diff --git a/.cspell.json b/.cspell.json new file mode 100644 index 00000000..db16817a --- /dev/null +++ b/.cspell.json @@ -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" + ] +} \ No newline at end of file diff --git a/.env.sample b/.env.sample index a7a95a78..288c6a43 100644 --- a/.env.sample +++ b/.env.sample @@ -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='' diff --git a/.vscode/settings.json b/.vscode/settings.json index e0a7a74e..759f3075 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,5 +18,5 @@ "prettier.disableLanguages": ["javascript", "typescript"], "prettier.enable": true, "typescript.autoClosingTags": false, - "standard.enable": false + "standard.enable": false, } diff --git a/src/tasks/harbor/harbor.ts b/src/tasks/harbor/harbor.ts index 069473a8..97c3c783 100644 --- a/src/tasks/harbor/harbor.ts +++ b/src/tasks/harbor/harbor.ts @@ -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 = { @@ -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', @@ -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) @@ -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 { +async function createSystemRobotSecret(): Promise { 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 { - let robotSecret = (await getSecret(secretName, namespace)) as RobotSecret +async function createProjectRobotSecret(teamId: string, projectId: string): Promise { + 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 { + 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 { @@ -130,44 +181,64 @@ async function ensureSecret(): Promise { // 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 { + 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 { - 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, }, } @@ -178,12 +249,16 @@ async function main(): Promise { 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 }), ) diff --git a/src/utils.ts b/src/utils.ts index 0343f54c..f808a276 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -126,7 +126,7 @@ export async function createPullSecret({ username, password, email: 'not@val.id', - auth: username + Buffer.from(password).toString('base64'), + auth: Buffer.from(`${username}:${password}`).toString('base64'), }, }, } @@ -134,7 +134,7 @@ export async function createPullSecret({ const secret = { ...new V1Secret(), metadata: { ...new V1ObjectMeta(), name }, - type: 'docker-registry', + type: 'kubernetes.io/dockerconfigjson', data: { '.dockerconfigjson': Buffer.from(JSON.stringify(data)).toString('base64'), }, diff --git a/src/validators.ts b/src/validators.ts index f71936d1..068b7c0d 100644 --- a/src/validators.ts +++ b/src/validators.ts @@ -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' }) @@ -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' })