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: ✨ store cluster secret in Vault #1504

Merged
merged 1 commit into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/server/src/resources/cluster/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,16 @@ export function getPublicClusters() {
})
}

export async function getClusterNamesByZoneId(zoneId: string) {
const clusterNames = await prisma.cluster.findMany({
where: { zoneId },
select: {
label: true,
},
})
return clusterNames.map(({ label }) => label)
}

export function getClusterByLabel(label: Cluster['label']) {
return prisma.cluster.findUnique({ where: { label } })
}
Expand Down
7 changes: 4 additions & 3 deletions apps/server/src/utils/hook-wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { Cluster, Kubeconfig, Project, ProjectRole, Zone } from '@prisma/client'
import type { ClusterObject, HookResult, KubeCluster, KubeUser, Project as ProjectPayload, RepoCreds, Repository, Store } from '@cpn-console/hooks'
import type { ClusterObject, HookResult, KubeCluster, KubeUser, Project as ProjectPayload, RepoCreds, Repository, Store, ZoneObject } from '@cpn-console/hooks'
import { hooks } from '@cpn-console/hooks'
import type { AsyncReturnType } from '@cpn-console/shared'
import { ProjectAuthorized, getPermsByUserRoles, resourceListToDict } from '@cpn-console/shared'
import { genericProxy } from './proxy.js'
import { archiveProject, getAdminPlugin, getClusterByIdOrThrow, getClustersAssociatedWithProject, getHookProjectInfos, getHookRepository, getProjectStore, getZoneByIdOrThrow, saveProjectStore, updateProjectClusterHistory, updateProjectCreated, updateProjectFailed, updateProjectWarning } from '@/resources/queries-index.js'
import { archiveProject, getAdminPlugin, getClusterByIdOrThrow, getClusterNamesByZoneId, getClustersAssociatedWithProject, getHookProjectInfos, getHookRepository, getProjectStore, getZoneByIdOrThrow, saveProjectStore, updateProjectClusterHistory, updateProjectCreated, updateProjectFailed, updateProjectWarning } from '@/resources/queries-index.js'
import type { ConfigRecords } from '@/resources/project-service/business.js'
import { dbToObj } from '@/resources/project-service/business.js'

Expand Down Expand Up @@ -121,7 +121,8 @@ const user = {

const zone = {
upsert: async (zoneId: Zone['id']) => {
const zone = await getZoneByIdOrThrow(zoneId)
const zone: ZoneObject = await getZoneByIdOrThrow(zoneId)
zone.clusterNames = await getClusterNamesByZoneId(zoneId)
const store = dbToObj(await getAdminPlugin())
return hooks.upsertZone.execute(zone, store)
},
Expand Down
1 change: 1 addition & 0 deletions packages/hooks/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface ZoneObject {
id: string
slug: string
argocdUrl: string
clusterNames?: string[]
}

export interface ClusterObject {
Expand Down
9 changes: 9 additions & 0 deletions plugins/argocd/src/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@ import { getConfig, getK8sApi } from './utils.js'
export const upsertCluster: StepCall<ClusterObject> = async (payload) => {
try {
const cluster = payload.args
const { vault } = payload.apis
if (cluster.label === inClusterLabel) {
await deleteClusterSecret(cluster.secretName)
} else {
await createClusterSecret(cluster)
}
const clusterData = {
name: cluster.label,
clusterResources: cluster.clusterResources.toString(),
server: cluster.cluster.server,
config: JSON.stringify(convertConfig(cluster)),
}
await vault.write(clusterData, `clusters/cluster-${cluster.label}/argocd-cluster-secret`)
return {
status: {
result: 'OK',
Expand All @@ -32,6 +40,7 @@ export const deleteCluster: StepCall<ClusterObject> = async (payload) => {
try {
const secretName = payload.args.secretName
await deleteClusterSecret(secretName)
await payload.apis.vault.destroy(`clusters/cluster-${payload.args.label}/argocd-cluster-secret`)
return {
status: {
result: 'OK',
Expand Down
2 changes: 1 addition & 1 deletion plugins/argocd/src/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ async function ensureInfraEnvValues(project: Project, environment: Environment,
const cluster = getCluster(project, environment)
const infraProject = await gitlabApi.getProjectById(repoId)
const valueFilePath = getValueFilePath(project, cluster, environment)
const vaultCredentials = await vaultApi.getCredentials()
const vaultCredentials = await vaultApi.Project.getCredentials()
const repositories: ArgoRepoSource[] = await Promise.all([
getArgoRepoSource('infra-apps', environment.name, gitlabApi),
...sourceRepos.map(repo => getArgoRepoSource(repo.internalRepoName, environment.name, gitlabApi)),
Expand Down
6 changes: 6 additions & 0 deletions plugins/argocd/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import infos from './infos.js'
import monitor from './monitor.js'
import { deleteProject, upsertProject } from './functions.js'
import { deleteCluster, upsertCluster } from './cluster.js'
import { upsertZone } from './zone.js'

export const plugin: Plugin = {
infos,
Expand All @@ -27,6 +28,11 @@ export const plugin: Plugin = {
main: deleteCluster,
},
},
upsertZone: {
steps: {
main: upsertZone,
},
},
},
monitor,
}
Expand Down
29 changes: 29 additions & 0 deletions plugins/argocd/src/zone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { type StepCall, type ZoneObject, parseError } from '@cpn-console/hooks'
import { dump } from 'js-yaml'

export const upsertZone: StepCall<ZoneObject> = async (payload) => {
try {
const zone = payload.args
const { gitlab, vault } = payload.apis
const values = {
vault: await vault.getCredentials(),
clusters: zone.clusterNames,
}
const zoneRepo = await gitlab.getOrCreateInfraProject(zone.slug)
await gitlab.commitCreateOrUpdate(zoneRepo.id, dump(values), 'argocd-values.yaml')
return {
status: {
result: 'OK',
message: 'Zone argocd configuration created/updated',
},
}
} catch (error) {
return {
error: parseError(error),
status: {
result: 'KO',
message: 'Failed create/update zone argocd configuration',
},
}
}
}
2 changes: 1 addition & 1 deletion plugins/gitlab/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const plugin: Plugin = {
upsertZone: {
api: () => new GitlabZoneApi(),
steps: {
main: upsertZone,
pre: upsertZone,
post: commitFiles,
},
},
Expand Down
91 changes: 61 additions & 30 deletions plugins/vault/src/class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ export class VaultApi extends PluginApi {
return this.token
}

protected async destroy(path: string, kvName: string) {
if (path.startsWith('/'))
path = path.slice(1)
return await this.axios({
method: 'delete',
url: `/v1/${kvName}/metadata/${path}`,
headers: { 'X-Vault-Token': await this.getToken() },
})
}

Kv = {
upsert: async (kvName: string) => {
const token = await this.getToken()
Expand Down Expand Up @@ -132,7 +142,6 @@ export class VaultApi extends PluginApi {
headers: { 'X-Vault-Token': await this.getToken() },
})
},

delete: async (roleName: string) => {
await this.axios.delete(
`/v1/auth/approle/role/${roleName}`,
Expand All @@ -141,6 +150,25 @@ export class VaultApi extends PluginApi {
},
)
},
getCredentials: async (roleName: string) => {
const { data: dataRole } = await this.axios.get(
`/v1/auth/approle/role/${roleName}/role-id`,
{
headers: { 'X-Vault-Token': await this.getToken() },
},
)
const { data: dataSecret } = await this.axios.put(
`/v1/auth/approle/role/${roleName}/secret-id`,
'null', // Force empty data
{
headers: { 'X-Vault-Token': await this.getToken() },
},
)
return {
roleId: dataRole.data?.role_id,
secretId: dataSecret.data?.secret_id,
}
},
}
}

Expand Down Expand Up @@ -233,35 +261,7 @@ export class VaultProjectApi extends VaultApi {
public async destroy(path: string = '/') {
if (path.startsWith('/'))
path = path.slice(1)
return this.axios({
method: 'delete',
url: `/v1/${this.coreKvName}/metadata/${this.projectRootDir}/${this.basePath}/${path}`,
headers: { 'X-Vault-Token': await this.getToken() },

})
}

public async getCredentials(): Promise<AppRoleCredentials> {
const appRoleEnabled = await isAppRoleEnabled(this.axios, await this.getToken())
if (!appRoleEnabled) return this.defaultAppRoleCredentials
const { data: dataRole } = await this.axios.get(
`/v1/auth/approle/role/${this.roleName}/role-id`,
{
headers: { 'X-Vault-Token': await this.getToken() },
},
)
const { data: dataSecret } = await this.axios.put(
`/v1/auth/approle/role/${this.roleName}/secret-id`,
'null', // Force empty data
{
headers: { 'X-Vault-Token': await this.getToken() },
},
)
return {
...this.defaultAppRoleCredentials,
roleId: dataRole.data?.role_id,
secretId: dataSecret.data?.secret_id,
}
return super.destroy(`${this.projectRootDir}/${this.basePath}/${path}`, this.coreKvName)
}

Project = {
Expand All @@ -285,6 +285,15 @@ export class VaultProjectApi extends VaultApi {
await this.Group.delete()
await this.Role.delete(this.roleName)
},
getCredentials: async () => {
const appRoleEnabled = await isAppRoleEnabled(this.axios, await this.getToken())
if (!appRoleEnabled) return this.defaultAppRoleCredentials
const creds = await this.Role.getCredentials(this.roleName)
return {
...this.defaultAppRoleCredentials,
...creds,
}
},
}

Group = {
Expand Down Expand Up @@ -370,4 +379,26 @@ export class VaultZoneApi extends VaultApi {
)
return await response.data
}

public async destroy(path: string = '/') {
if (path.startsWith('/'))
path = path.slice(1)
return super.destroy(path, this.kvName)
}

public async getCredentials() {
const appRoleEnabled = await isAppRoleEnabled(this.axios, await this.getToken())
if (!appRoleEnabled) {
return {
url: getConfig().publicUrl,
kvName: this.kvName,
}
}
const creds = await this.Role.getCredentials(this.roleName)
return {
url: getConfig().publicUrl,
kvName: this.kvName,
...creds,
}
}
}
2 changes: 1 addition & 1 deletion plugins/vault/src/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const deployAuth: StepCall<Project> = async (payload) => {
if (!payload.apis.vault) {
throw new Error('no Vault available')
}
const appRoleCreds = await payload.apis.vault.getCredentials()
const appRoleCreds = await payload.apis.vault.Project.getCredentials()
if (getConfig().disableVaultSecrets) {
return okStatus
}
Expand Down
10 changes: 8 additions & 2 deletions plugins/vault/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DefaultArgs, Plugin, Project, ProjectLite, ZoneObject } from '@cpn-console/hooks'
import type { ClusterObject, DefaultArgs, Plugin, Project, ProjectLite, ZoneObject } from '@cpn-console/hooks'
import { archiveDsoProject, deleteZone, deployAuth, getSecrets, upsertProject, upsertZone } from './functions.js'
import infos from './infos.js'
import monitor from './monitor.js'
Expand Down Expand Up @@ -38,6 +38,12 @@ export const plugin: Plugin = {
main: deleteZone,
},
},
upsertCluster: {
api: (c: ClusterObject) => new VaultZoneApi(c.zone.slug),
},
deleteCluster: {
api: (c: ClusterObject) => new VaultZoneApi(c.zone.slug),
},
},
monitor,
}
Expand All @@ -46,7 +52,7 @@ declare module '@cpn-console/hooks' {
interface HookPayloadApis<Args extends DefaultArgs> {
vault: Args extends (ProjectLite | Project)
? VaultProjectApi
: Args extends (ZoneObject)
: Args extends (ZoneObject | ClusterObject)
? VaultZoneApi
: never
}
Expand Down
Loading