Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create robot accounts with push creds for teams #77

Merged
merged 19 commits into from
Mar 6, 2023
41 changes: 41 additions & 0 deletions src/k8s.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,47 @@ export async function createPullSecret({
}
}

export async function createPushSecret({
namespace,
name,
server,
password,
username = '_json_key',
}: {
namespace: string
name: string
server: string
password: string
username?: string
}): Promise<void> {
const client = k8s.core()
// create data structure for secret
const data = {
auths: {
[server]: {
username,
password,
email: 'not@val.id',
auth: Buffer.from(`${username}:${password}`).toString('base64'),
},
},
}
// create the secret
const secret = {
...new V1Secret(),
metadata: { ...new V1ObjectMeta(), name },
type: 'kubernetes.io/dockerconfigjson',
data: {
'.dockerconfigjson': Buffer.from(JSON.stringify(data)).toString('base64'),
},
}
// eslint-disable-next-line no-useless-catch
try {
await client.createNamespacedSecret(namespace, secret)
} catch (e) {
throw new Error(`Secret '${name}' already exists in namespace '${namespace}'`)
}
}
export async function getPullSecrets(namespace: string): Promise<Array<any>> {
const client = k8s.core()
const saRes = await client.readNamespacedServiceAccount('default', namespace)
Expand Down
106 changes: 90 additions & 16 deletions src/tasks/harbor/harbor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// eslint-disable @typescript-eslint/camelcase
// es
dennisvankekem marked this conversation as resolved.
Show resolved Hide resolved

import {
ConfigureApi,
Expand Down Expand Up @@ -100,7 +101,8 @@ const config: any = {

const systemNamespace = 'harbor'
const systemSecretName = 'harbor-robot-admin'
const projectSecretName = 'harbor-pullsecret'
const projectPullSecretName = 'harbor-pullsecret'
const projectPushSecretName = 'harbor-pushsecret'
const harborBaseUrl = `${env.HARBOR_BASE_URL}/api/v2.0`
const harborHealthUrl = `${harborBaseUrl}/systeminfo`
const robotApi = new RobotApi(env.HARBOR_USER, env.HARBOR_PASSWORD, harborBaseUrl)
Expand Down Expand Up @@ -135,7 +137,7 @@ async function createSystemRobotSecret(): Promise<RobotSecret> {
* Create Harbor system robot account that is scoped to a given Harbor project
* @param projectName Harbor project name
*/
async function createTeamRobotAccount(projectName: string): Promise<RobotCreated> {
async function createTeamPullRobotAccount(projectName: string): Promise<RobotCreated> {
const projectRobot: RobotCreate = {
name: `${projectName}-pull`,
duration: -1,
Expand Down Expand Up @@ -165,15 +167,63 @@ async function createTeamRobotAccount(projectName: string): Promise<RobotCreated
await doApiCall(errors, `Deleting previous robot account ${fullName}`, () => robotApi.deleteRobot(existingId))
}

const robotAccount = (await doApiCall(errors, `Creating robot account ${fullName} with project level perms`, () =>
const robotPullAccount = (await doApiCall(errors, `Creating robot account ${fullName} with project level perms`, () =>
robotApi.createRobot(projectRobot),
)) as RobotCreated
if (!robotAccount?.id) {
if (!robotPullAccount?.id) {
throw new Error(
j-zimnowoda marked this conversation as resolved.
Show resolved Hide resolved
`RobotAccount already exists and should have been deleted beforehand. This happens when more than 100 robot accounts exist.`,
`RobotPullAccount already exists and should have been deleted beforehand. This happens when more than 100 robot accounts exist.`,
j-zimnowoda marked this conversation as resolved.
Show resolved Hide resolved
)
}
return robotAccount
return robotPullAccount
}

/**
* Create Harbor system robot account that is scoped to a given Harbor project
* @param projectName Harbor project name
*/
async function createTeamPushRobotAccount(projectName: string): Promise<any> {
j-zimnowoda marked this conversation as resolved.
Show resolved Hide resolved
const projectRobot: RobotCreate = {
name: `${projectName}-push`,
duration: -1,
description: 'Allow team to push from its own registry',
j-zimnowoda marked this conversation as resolved.
Show resolved Hide resolved
disable: false,
level: 'system',
permissions: [
{
kind: 'project',
namespace: projectName,
access: [
{
resource: 'repository',
action: 'push',
},
{
resource: 'repository',
action: 'pull',
},
],
},
],
}
const fullName = `${robotPrefix}${projectRobot.name}`

const { body: robotList } = await robotApi.listRobot(undefined, undefined, undefined, undefined, 100)
const existing = robotList.find((i) => i.name === fullName)

if (existing?.name) {
return existing
}

const robotPushAccount = (await doApiCall(errors, `Creating robot account ${fullName} with project level perms`, () =>
robotApi.createRobot(projectRobot),
)) as RobotCreated
if (!robotPushAccount?.id) {
throw new Error(
`RobotPushAccount already exists and should have been deleted beforehand. This happens when more than 100 robot accounts exist.`,
)
}
return robotPushAccount
}

/**
Expand Down Expand Up @@ -213,21 +263,44 @@ async function getBearerToken(): Promise<HttpBearerAuth> {
* @param namespace Kubernetes namespace where pull secret is created
* @param projectName Harbor project name
*/
async function ensureTeamRobotAccountSecret(namespace: string, projectName): Promise<void> {
const k8sSecret = await getSecret(projectSecretName, namespace)
async function ensureTeamPullRobotAccountSecret(namespace: string, projectName): Promise<void> {
const k8sSecret = await getSecret(projectPullSecretName, namespace)
if (k8sSecret) {
console.debug(`Deleting secret/${projectPullSecretName} from ${namespace} namespace`)
await k8s.core().deleteNamespacedSecret(projectPullSecretName, namespace)
}

const robotPullAccount = await createTeamPullRobotAccount(projectName)
console.debug(`Creating secret/${projectPullSecretName} at ${namespace} namespace`)
await createPullSecret({
namespace,
name: projectPullSecretName,
server: `${env.HARBOR_BASE_REPO_URL}`,
username: robotPullAccount.name!,
password: robotPullAccount.secret!,
})
}

/**
* Ensure that Harbor robot account and corresponding Kubernetes pull secret exist
* @param namespace Kubernetes namespace where push secret is created
* @param projectName Harbor project name
*/
async function ensureTeamPushRobotAccountSecret(namespace: string, projectName): Promise<void> {
const k8sSecret = await getSecret(projectPushSecretName, namespace)
if (k8sSecret) {
console.debug(`Deleting secret/${projectSecretName} from ${namespace} namespace`)
await k8s.core().deleteNamespacedSecret(projectSecretName, namespace)
console.debug(`Deleting secret/${projectPushSecretName} from ${namespace} namespace`)
await k8s.core().deleteNamespacedSecret(projectPushSecretName, namespace)
}

const robotAccount = await createTeamRobotAccount(projectName)
console.debug(`Creating secret/${projectSecretName} at ${namespace} namespace`)
const robotPushAccount = await createTeamPushRobotAccount(projectName)
console.debug(`Creating secret/${projectPushSecretName} at ${namespace} namespace`)
await createPullSecret({
namespace,
name: projectSecretName,
name: projectPushSecretName,
server: `${env.HARBOR_BASE_REPO_URL}`,
username: robotAccount.name!,
password: robotAccount.secret!,
username: robotPushAccount.name!,
password: robotPushAccount.secret!,
})
}

Expand Down Expand Up @@ -281,7 +354,8 @@ async function main(): Promise<void> {
() => memberApi.createProjectMember(projectId, undefined, undefined, projAdminMember),
)

await ensureTeamRobotAccountSecret(teamNamespce, projectName)
await ensureTeamPullRobotAccountSecret(teamNamespce, projectName)
await ensureTeamPushRobotAccountSecret(teamNamespce, projectName)

return null
}),
Expand Down