Skip to content

Commit 2a84d20

Browse files
authored
Merge branch 'main' into ci-update-ingress-nginx-to-4.15.1
2 parents 2355b51 + 1e0a51d commit 2a84d20

File tree

6 files changed

+157
-6
lines changed

6 files changed

+157
-6
lines changed

helmfile.d/snippets/defaults.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1190,4 +1190,4 @@ environments:
11901190
aiEnabled: false
11911191
users: []
11921192
versions:
1193-
specVersion: 58
1193+
specVersion: 59

src/cmd/migrate.ts

Lines changed: 150 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ApiException } from '@kubernetes/client-node'
1+
import { ApiException, PatchStrategy, setHeaderOptions } from '@kubernetes/client-node'
22
import { encryptSecretItem } from '@linode/kubeseal-encrypt'
33
import { randomUUID } from 'crypto'
44
import { diff } from 'deep-diff'
@@ -19,9 +19,9 @@ import { BasicArguments, getParsedArgs, setParsedArgs } from 'src/common/yargs'
1919
import { v4 as uuidv4 } from 'uuid'
2020
import { parse } from 'yaml'
2121
import { Argv } from 'yargs'
22-
import { $, cd } from 'zx'
22+
import { $, cd, sleep } from 'zx'
2323
import { APL_OPERATOR_NS, ARGOCD_APP_PARAMS } from '../common/constants'
24-
import { getK8sSecret, getSealedSecretsPEM, k8s } from '../common/k8s'
24+
import { getArgoCdApp, getK8sSecret, getSealedSecretsPEM, k8s, setArgoCdAppSync } from '../common/k8s'
2525

2626
const cmdName = getFilename(__filename)
2727

@@ -520,6 +520,107 @@ async function appExists(name: string): Promise<boolean> {
520520
)
521521
}
522522

523+
async function hasPvcWithStorageClass(
524+
namespace: string,
525+
labelSelector: string,
526+
storageClassName: string,
527+
): Promise<boolean> {
528+
try {
529+
const pvcList = await k8s.core().listNamespacedPersistentVolumeClaim({ namespace, labelSelector })
530+
return (pvcList?.items || []).some((pvc) => pvc?.spec?.storageClassName === storageClassName)
531+
} catch (error) {
532+
if (error instanceof ApiException && error.code === 404) {
533+
return false
534+
}
535+
throw error
536+
}
537+
}
538+
539+
async function waitForPodsDeletion(namespace: string, labelSelector: string, timeoutMs = 300000): Promise<void> {
540+
const start = Date.now()
541+
while (Date.now() - start < timeoutMs) {
542+
const pods = await k8s.core().listNamespacedPod({ namespace, labelSelector })
543+
if ((pods.items || []).length === 0) return
544+
await sleep(5000)
545+
}
546+
}
547+
548+
async function waitForStatefulSetDeletion(name: string, namespace: string, timeoutMs = 300000): Promise<void> {
549+
const start = Date.now()
550+
while (Date.now() - start < timeoutMs) {
551+
const exists = await checkExists(async () => await k8s.app().readNamespacedStatefulSet({ name, namespace }))
552+
if (!exists) return
553+
await sleep(5000)
554+
}
555+
}
556+
557+
async function deletePvcsByLabel(namespace: string, labelSelector: string): Promise<void> {
558+
const pvcList = await k8s.core().listNamespacedPersistentVolumeClaim({ namespace, labelSelector })
559+
for (const pvc of pvcList.items || []) {
560+
const name = pvc?.metadata?.name
561+
if (!name) continue
562+
try {
563+
await k8s.core().deleteNamespacedPersistentVolumeClaim({ name, namespace })
564+
} catch (error) {
565+
if (!(error instanceof ApiException && error.code === 404)) throw error
566+
}
567+
}
568+
}
569+
570+
async function migrateStatefulSetPvc(opts: {
571+
appName: string
572+
statefulSetName: string
573+
namespace: string
574+
pvcLabelSelector: string
575+
d: ReturnType<typeof terminal>
576+
}): Promise<void> {
577+
const parsedArgs = getParsedArgs()
578+
if (parsedArgs?.dryRun || parsedArgs?.local) {
579+
return
580+
}
581+
582+
let syncDisabled = false
583+
try {
584+
const app = await getArgoCdApp(opts.appName, k8s.custom())
585+
if (app) {
586+
await setArgoCdAppSync(opts.appName, false, k8s.custom())
587+
syncDisabled = true
588+
} else {
589+
opts.d.info(`Argo CD application ${opts.appName} not found. Skipping sync disable.`)
590+
}
591+
592+
await k8s.app().patchNamespacedStatefulSet(
593+
{
594+
name: opts.statefulSetName,
595+
namespace: opts.namespace,
596+
body: { spec: { replicas: 0 } },
597+
},
598+
setHeaderOptions('Content-Type', PatchStrategy.StrategicMergePatch),
599+
)
600+
601+
await waitForPodsDeletion(opts.namespace, opts.pvcLabelSelector)
602+
603+
try {
604+
await k8s.app().deleteNamespacedStatefulSet({ name: opts.statefulSetName, namespace: opts.namespace })
605+
} catch (error) {
606+
if (!(error instanceof ApiException && error.code === 404)) throw error
607+
}
608+
609+
await waitForStatefulSetDeletion(opts.statefulSetName, opts.namespace)
610+
await deletePvcsByLabel(opts.namespace, opts.pvcLabelSelector)
611+
} catch (error) {
612+
throw error
613+
} finally {
614+
if (syncDisabled) {
615+
try {
616+
await setArgoCdAppSync(opts.appName, true, k8s.custom())
617+
} catch (error) {
618+
opts.d.warn(`Failed to re-enable Argo CD sync for ${opts.appName}: ${error}`)
619+
}
620+
}
621+
}
622+
}
623+
523624
export async function addAplOperator(): Promise<void> {
524625
const d = terminal('addAplOperator')
525626
if (await namespaceExists(APL_OPERATOR_NS)) {
@@ -713,6 +814,51 @@ const createCatalogSealedSecret = async (
713814
writeFileSync(sealedSecretPath, objectToYaml(sealedSecret))
714815
}
715816

817+
// This migration changes PVCs when using Linode for gitea-valkey and oauth2-proxy-redis-server to use linode-block-storage instead of linode-block-storage-retain
818+
const valkeyAndOauth2RedisPVCMigration = async (values: Record<string, any>): Promise<void> => {
819+
const d = terminal('valkeyAndOauth2RedisPVCMigration')
820+
if (env.DISABLE_SYNC) {
821+
d.info('Skipping Valkey and Oauth2 Redis PVC migration in dev/test environment')
822+
return
823+
}
824+
const giteaEnabled = values?.apps?.gitea?.enabled
825+
const isLinode = values?.cluster?.provider === 'linode'
826+
const legacyStorageClass = 'linode-block-storage-retain'
827+
if (isLinode) {
828+
d.info('Changing PVC storage class to linode-block-storage for Gitea and OAuth2 Proxy Redis Server')
829+
if (giteaEnabled) {
830+
const hasLegacyGiteaPvc = await hasPvcWithStorageClass(
831+
'gitea',
832+
'app.kubernetes.io/name=valkey',
833+
legacyStorageClass,
834+
)
835+
if (hasLegacyGiteaPvc) {
836+
await migrateStatefulSetPvc({
837+
appName: 'gitea-gitea-valkey',
838+
statefulSetName: 'gitea-valkey-primary',
839+
namespace: 'gitea',
840+
pvcLabelSelector: 'app.kubernetes.io/name=valkey',
841+
d,
842+
})
843+
} else {
844+
d.info(`Skipping gitea PVC migration: no PVC found with storageClass ${legacyStorageClass}`)
845+
}
846+
}
847+
const hasLegacyOauthPvc = await hasPvcWithStorageClass('istio-system', 'app=redis', legacyStorageClass)
848+
if (hasLegacyOauthPvc) {
849+
await migrateStatefulSetPvc({
850+
appName: 'istio-system-oauth2-proxy',
851+
statefulSetName: 'oauth2-proxy-redis-ha-server',
852+
namespace: 'istio-system',
853+
pvcLabelSelector: 'app=redis',
854+
d,
855+
})
856+
} else {
857+
d.info(`Skipping oauth2-proxy redis PVC migration: no PVC found with storageClass ${legacyStorageClass}`)
858+
}
859+
}
860+
}
861+
716862
const setDefaultAplCatalog = async (values: Record<string, any>): Promise<void> => {
717863
const d = terminal('setDefaultAplCatalog')
718864
const gitea = values?.apps?.gitea as { adminUsername?: string; adminPassword?: string } | undefined
@@ -843,6 +989,7 @@ const customMigrationFunctions: Record<string, CustomMigrationFunction> = {
843989
workloadValuesMigration,
844990
setLokiStorageSchemaMigration,
845991
setDefaultAplCatalog,
992+
valkeyAndOauth2RedisPVCMigration,
846993
addLinodeNBAnnotations,
847994
setIngressDefault,
848995
}

tests/fixtures/env/settings/versions.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ metadata:
33
name: versions
44
labels: {}
55
spec:
6-
specVersion: 58
6+
specVersion: 59

values-changes.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,3 +455,6 @@ changes:
455455
- version: 58
456456
additions:
457457
- apps.tekton.enabled: true
458+
- version: 59
459+
customFunctions:
460+
- valkeyAndOauth2RedisPVCMigration

values/gitea/gitea-valkey.gotmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{{- $v := .Values }}
22

33
global:
4+
defaultStorageClass: {{ if eq $v.cluster.provider "linode" }}linode-block-storage{{ else }}{{ $v.cluster.defaultStorageClass | default "" }}{{ end }}
45
security:
56
allowInsecureImages: true
67

values/oauth2-proxy/oauth2-proxy.gotmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ redis-ha:
154154
persistentVolume:
155155
{{- if $r.persistentVolumeSize }}
156156
size: {{ $r.persistentVolumeSize }}
157-
storageClass: {{ $v.cluster.defaultStorageClass }}
157+
storageClass: {{ if eq $v.cluster.provider "linode" }}linode-block-storage{{ else }}{{ $v.cluster.defaultStorageClass | default "" }}{{ end }}
158158
{{- else }}
159159
enabled: false
160160
{{- end }}

0 commit comments

Comments
 (0)