1import * as _ from 'lodash-es' 19type WithResourceAndCluster = { resource: KubeResource, cluster_name: string } 21const getApisForResource = async ({resource, cluster_name}: WithResourceAndCluster) => { 24 if (offlineReason) throw new PtErr(`cluster ${cluster_name} is offline`, {cluster_name, offlineReason, resource: inspect2KubeRes({resource, cluster_name})}) 28type ObjectApiMethodCatchProps = WithResourceAndCluster & { method_name: string } 29export const objectApiMethodCatch = async ({method_name, resource, cluster_name}: ObjectApiMethodCatchProps) => { 30 const {objectApi} = await getApisForResource({resource, cluster_name}) 32 if (method_name == 'create' || method_name == 'patch') { 33 const contents = JSON.stringify(resource, null, 2) 36 // TODO cleanup: server-side apply avoids the 256KB annotation size limit when shipping 37 // large ConfigMaps (e.g. worker build artifacts via inclContentH). Could scope this to 38 // only large ConfigMaps instead of applying globally, but server-side apply is the 39 // modern k8s standard anyway. 40 const cmdA = [cluster_name, 'apply', '--server-side', '--validate=false', '--force-conflicts', '-f', tmpResPath] 41 let result: {isSuccess?: boolean, stderr?: string, code?: number} | undefined 42 await noOutCmdCtx.run({noOutput: true} as any, async () => { 46 if (r && !r.isSuccess) throw new Error(r.stderr || `kubectl code=${r.code}`) 48 }, cluster_name) as typeof result 50 throw new PtErr('kubectl apply failed', {resource: inspect2KubeRes({resource, cluster_name}), stderr: String(err?.message || err)}) 53 if (result && !result.isSuccess) { 54 throw new PtErr('kubectl apply failed', {resource: inspect2KubeRes({resource, cluster_name}), stderr: result.stderr}) 58 // ^properly throws when invalid backup: param of zalando specified. unlike this code: 61 if (method_name == 'recreate') { 62 await res1Action({resource, action: 'delete', cluster_name}) 63 method_name = 'create' 67 return await withKubeRetry(() => (objectApi as any)[method_name](resource), cluster_name) 70 if (!err.body) throw err 74 const {reason} = bodyH as { reason?: string } 75 if (reason == 'AlreadyExists') { 79 throw new PtErr('!doObjectApiMethodCatch', { 81 bodyH, resource: inspect2KubeRes({resource, cluster_name}) 86const cantPatch = ({resource}: {resource: KubeResource}) => { 87 const {kind} = resource 89 const type = _.get(resource, 'spec.type') 91 const isNodePort = kind == 'Service' && type == 'NodePort' 93 return isNodePort || _.includes([ 96 'PersistentVolumeClaim', 101export const applyResource = async ({resource, cluster_name}: WithResourceAndCluster) => { 104 let action = 'create' 106 if (cantPatch({resource})) { 107 action = 'cantupdate' 108 } else if (_.includes(['letsencrypt-prod', 'le-wildcard-issuer'], _.get(resource, 'metadata.name'))) { 110 } else if (resource.kind === 'Deployment' && _.get(resource, 'spec.strategy.type') === 'Recreate' && _.get(existing, 'spec.strategy.type') !== 'Recreate') { 117 const {name} = resource.metadata, {kind} = resource 118 if (action == 'cantupdate') { 119 if (kind == 'Certificate') { 120 const existingDns = _.get(existing, 'spec.dnsNames') as string[] | undefined, desiredDns = _.get(resource, 'spec.dnsNames') as string[] | undefined 121 if (!_.isEqual(_.sortBy(existingDns ?? []), _.sortBy(desiredDns ?? []))) { 122 betLog(`can't patch Certificate ${name}; delete + re-apply to change SANs`, {existingDns, desiredDns}) 125 return {action, cluster_name, name, kind} 128 await objectApiMethodCatch({method_name: action, resource, cluster_name}) 129 return {action, cluster_name, name, kind} 132type ObjectApiCallProps = WithResourceAndCluster & { objectApiMethod: string } 133export const objectApiCall = async ({resource, objectApiMethod, cluster_name}: ObjectApiCallProps) => { 134 const {objectApi} = await getApisForResource({resource, cluster_name}) 137 ;({body} = await (objectApi as any)[objectApiMethod](resource)) 139 if (!err.body) throw err 141 if (reason == 'AlreadyExists') { 148export const inspect2KubeRes = ({resource, cluster_name}: {resource: KubeResource, cluster_name?: string}) => { 153export const delete2Resource = async ({resource, cluster_name}: WithResourceAndCluster) => { 154 const resp = await objectApiCall({resource, objectApiMethod: 'delete', cluster_name}) as { code?: number, status?: string } | undefined 155 const {name} = resource.metadata, {kind} = resource 156 const action = (() => { 157 if (!resp) return 'deleted' 158 const {code, status} = resp 159 if (status == 'Success') return 'deleted' 160 if (code == 404) return 'notexist' 163 return {action, cluster_name, name, kind}