Skip to content

Commit

Permalink
Merge pull request #1504 from cloud-pi-native/feat/kubeconfig-in-vault-2
Browse files Browse the repository at this point in the history
feat: ✨ store cluster secret in Vault
  • Loading branch information
ArnaudTA authored Jan 20, 2025
2 parents 80dfd78 + cea79ef commit 0b1a519
Show file tree
Hide file tree
Showing 11 changed files with 131 additions and 38 deletions.
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

0 comments on commit 0b1a519

Please sign in to comment.