Skip to content

Commit 891be57

Browse files
authored
feat(k8s)!: action results caching in Garden Cloud V1 (#6960)
* feat: new endpoints in `CloudApi` * refactor: change contract of `CacheStorage` * chore: simplify logging No need to create action log instance every time. * refactor: make `CacheStorage` aware of its value shape * chore: initial impl of Cloud-based cache storage * refactor: change the signature of result cache getter Accept plugin context as a parameter. TODO: consider storing the cache in `PluginContext` * chore: add todo-comments for future refactoring * refactor: helper function to create `ResultCache` instance * feat: init Garden Cloud cache if logged in * chore: log details on the cache storage while creation * refactor: unify error handling on the `CacheStorage` level
1 parent 6375413 commit 891be57

File tree

17 files changed

+372
-74
lines changed

17 files changed

+372
-74
lines changed

core/src/cloud/api.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { LogLevel } from "../logger/logger.js"
3737
import { getStoredAuthToken, saveAuthToken } from "./auth.js"
3838
import type { StringMap } from "../config/common.js"
3939
import { styles } from "../logger/styles.js"
40-
import { HTTPError } from "got"
40+
import { HTTPError, RequestError } from "got"
4141
import type { Garden } from "../garden.js"
4242
import type { ApiCommandError } from "../commands/cloud/helpers.js"
4343
import { enumerate } from "../util/enumerate.js"
@@ -46,6 +46,7 @@ import type { ApiFetchOptions } from "./http-client.js"
4646
import { GardenCloudHttpClient } from "./http-client.js"
4747
import { getCloudDistributionName, getCloudLogSectionName } from "./util.js"
4848
import type { GrowCloudApiFactory } from "./grow/api.js"
49+
import type { JsonObject } from "type-fest"
4950

5051
export class CloudApiDuplicateProjectsError extends CloudApiError {}
5152

@@ -792,6 +793,47 @@ export class GardenCloudApi {
792793
})
793794
}
794795
}
796+
797+
async createActionResult(request: CreateCachedActionRequest): Promise<CreateCachedActionResponse> {
798+
try {
799+
return await this.post<CreateCachedActionResponse>(`/action-cache`, { body: request })
800+
} catch (e) {
801+
if (!(e instanceof RequestError)) {
802+
throw e
803+
}
804+
805+
const rootCause = extractErrorMessageBodyFromGotError(e) ?? e.message
806+
throw new CloudApiError({
807+
message: `Error storing data to Team Cache V1; code=${e.code}; cause: ${rootCause}`,
808+
responseStatusCode: e.response?.statusCode,
809+
})
810+
}
811+
}
812+
813+
async getActionResult({
814+
organizationId,
815+
projectId,
816+
schemaVersion,
817+
actionRef,
818+
actionType,
819+
cacheKey,
820+
}: GetCachedActionRequest): Promise<GetCachedActionResponse> {
821+
try {
822+
return await this.get<GetCachedActionResponse>(
823+
`/action-cache?organizationId=${organizationId}&projectId=${projectId}&schemaVersion=${schemaVersion}&actionRef=${actionRef}&actionType=${actionType}&cacheKey=${cacheKey}`
824+
)
825+
} catch (e) {
826+
if (!(e instanceof RequestError)) {
827+
throw e
828+
}
829+
830+
const rootCause = extractErrorMessageBodyFromGotError(e) ?? e.message
831+
throw new CloudApiError({
832+
message: `Error getting data from Team Cache V1; code=${e.code}; cause: ${rootCause}`,
833+
responseStatusCode: e.response?.statusCode,
834+
})
835+
}
836+
}
795837
}
796838

797839
// TODO(cloudbuilder): import these from api-types
@@ -830,3 +872,33 @@ export type CloudBuilderNotAvailableV2 = {
830872
reason: string
831873
}
832874
export type CloudBuilderAvailabilityV2 = CloudBuilderAvailableV2 | CloudBuilderNotAvailableV2
875+
876+
export interface CreateCachedActionRequest {
877+
organizationId: string
878+
projectId: string
879+
schemaVersion: string
880+
actionType: string
881+
actionRef: string
882+
cacheKey: string
883+
result: unknown // Must be JSON-serializable.
884+
// These should be ISO timestamps (which are always in the UTC timezone).
885+
startedAt: string
886+
completedAt: string
887+
}
888+
889+
export interface CreateCachedActionResponse extends BaseResponse {}
890+
891+
export interface GetCachedActionRequest {
892+
organizationId: string
893+
projectId: string
894+
schemaVersion: string
895+
actionRef: string
896+
actionType: string
897+
cacheKey: string
898+
}
899+
900+
type CachedAction = { found: true; result: JsonObject } | { found: false; notFoundReason: string }
901+
902+
export interface GetCachedActionResponse extends BaseResponse {
903+
data: CachedAction
904+
}

core/src/plugins/kubernetes/container/run.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export const k8sContainerRun: RunActionHandler<"run", ContainerRunAction> = asyn
5151
})
5252

5353
if (action.getSpec("cacheResult")) {
54-
const runResultCache = getRunResultCache(ctx.gardenDirPath)
54+
const runResultCache = getRunResultCache(ctx)
5555
await runResultCache.store({
5656
ctx,
5757
log,

core/src/plugins/kubernetes/container/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const k8sContainerTest: TestActionHandler<"run", ContainerTestAction> = a
4444
})
4545

4646
if (action.getSpec("cacheResult")) {
47-
const testResultCache = getTestResultCache(ctx.gardenDirPath)
47+
const testResultCache = getTestResultCache(ctx)
4848
await testResultCache.store({
4949
ctx,
5050
log,

core/src/plugins/kubernetes/helm/helm-pod.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export const helmPodRunDefinition = (): RunActionDefinition<HelmPodRunAction> =>
5656
const result = await runOrTestWithChart({ ...params, ctx: k8sCtx, namespace: namespaceStatus.namespaceName })
5757

5858
if (action.getSpec("cacheResult")) {
59-
const runResultCache = getRunResultCache(ctx.gardenDirPath)
59+
const runResultCache = getRunResultCache(ctx)
6060
await runResultCache.store({
6161
ctx,
6262
log,
@@ -98,7 +98,7 @@ export const helmPodTestDefinition = (): TestActionDefinition<HelmPodTestAction>
9898
const result = await runOrTestWithChart({ ...params, ctx: k8sCtx, namespace: namespaceStatus.namespaceName })
9999

100100
if (action.getSpec("cacheResult")) {
101-
const testResultCache = getTestResultCache(ctx.gardenDirPath)
101+
const testResultCache = getTestResultCache(ctx)
102102
await testResultCache.store({
103103
ctx,
104104
log,

core/src/plugins/kubernetes/kubernetes-type/kubernetes-pod.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export const kubernetesPodRunDefinition = (): RunActionDefinition<KubernetesPodR
125125
const result = await runOrTestWithPod({ ...params, ctx: k8sCtx, namespace: namespaceStatus.namespaceName })
126126

127127
if (action.getSpec("cacheResult")) {
128-
const runResultCache = await getRunResultCache(ctx.gardenDirPath)
128+
const runResultCache = await getRunResultCache(ctx)
129129
await runResultCache.store({
130130
ctx,
131131
log,
@@ -173,7 +173,7 @@ export const kubernetesPodTestDefinition = (): TestActionDefinition<KubernetesPo
173173
const result = await runOrTestWithPod({ ...params, ctx: k8sCtx, namespace: namespaceStatus.namespaceName })
174174

175175
if (action.getSpec("cacheResult")) {
176-
const testResultCache = getTestResultCache(ctx.gardenDirPath)
176+
const testResultCache = getTestResultCache(ctx)
177177
await testResultCache.store({
178178
ctx,
179179
log,

core/src/plugins/kubernetes/results-cache-base.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { renderZodError } from "../../config/zod.js"
2121
import type { JsonObject } from "type-fest"
2222
import { GardenError } from "../../exceptions.js"
2323
import { fullHashStrings } from "../../vcs/vcs.js"
24+
import type { Action } from "../../actions/types.js"
2425

2526
export type CacheableAction = RunAction | TestAction
2627

@@ -37,6 +38,7 @@ export type SchemaVersion = `v${number}`
3738
export const currentResultSchemaVersion: SchemaVersion = "v1"
3839
export const kubernetesCacheEntrySchema = runResultSchemaZod
3940
export type KubernetesCacheEntrySchema = typeof kubernetesCacheEntrySchema
41+
export type KubernetesCacheEntry = z.output<KubernetesCacheEntrySchema>
4042

4143
export interface LoadResultParams<A extends CacheableAction, AdditionalKeyData> {
4244
ctx: PluginContext
@@ -70,38 +72,46 @@ export class StructuredCacheKey<AdditionalKeyData> {
7072
}
7173
}
7274

73-
export class CacheStorageError extends GardenError {
75+
export abstract class CacheStorageError extends GardenError {
7476
type = "cache-storage"
77+
78+
abstract describe(): string
7579
}
7680

77-
export interface CacheStorage {
81+
export interface CacheStorage<ResultShape> {
7882
/**
7983
* Returns a value associated with the {@code key},
8084
* or throws a {@link CacheStorageError} if no key was found or any error occurred.
8185
*/
82-
get(key: string): Promise<JsonObject>
86+
get(key: string, action: Action): Promise<JsonObject>
8387

8488
/**
8589
* Stores the value associated with the {@code key}.
8690
*
8791
* Returns the value back if it was written successfully,
8892
* or throws a {@link CacheStorageError} otherwise.
8993
*/
90-
put(key: string, value: JsonObject): Promise<JsonObject>
94+
put(key: string, value: ResultShape, action: Action): Promise<ResultShape>
9195

9296
/**
9397
* Removes a value associated with the {@code key}.
9498
*
9599
* Throws a {@link CacheStorageError} if any error occurred.
96100
*/
97-
remove(key: string): Promise<void>
101+
remove(key: string, action: Action): Promise<void>
98102
}
99103

100104
export class ResultCache<A extends CacheableAction, ResultSchema extends AnyZodObject, AdditionalKeyData> {
101-
private readonly cacheStorage: CacheStorage
105+
private readonly cacheStorage: CacheStorage<z.output<ResultSchema>>
102106
private readonly resultSchema: ResultSchema
103107

104-
constructor({ cacheStorage, resultSchema }: { cacheStorage: CacheStorage; resultSchema: ResultSchema }) {
108+
constructor({
109+
cacheStorage,
110+
resultSchema,
111+
}: {
112+
cacheStorage: CacheStorage<z.output<ResultSchema>>
113+
resultSchema: ResultSchema
114+
}) {
105115
this.cacheStorage = cacheStorage
106116
this.resultSchema = resultSchema
107117
}
@@ -116,7 +126,7 @@ export class ResultCache<A extends CacheableAction, ResultSchema extends AnyZodO
116126
The provided result doesn't match the expected schema.
117127
Here is the output: ${renderZodError(result.error)}
118128
`
119-
log.debug(errorMessage)
129+
log.verbose(errorMessage)
120130
return undefined
121131
}
122132

@@ -128,13 +138,13 @@ export class ResultCache<A extends CacheableAction, ResultSchema extends AnyZodO
128138
public async clear({ log, action, keyData }: ClearResultParams<A, AdditionalKeyData>): Promise<void> {
129139
const key = this.cacheKey({ action, keyData })
130140
try {
131-
await this.cacheStorage.remove(key)
141+
await this.cacheStorage.remove(key, action)
132142
} catch (e) {
133143
if (!(e instanceof CacheStorageError)) {
134144
throw e
135145
}
136146

137-
action.createLog(log).debug(e.message)
147+
log.verbose(`Error clearing results cache entry for key=${key}: ${e.describe()}`)
138148
}
139149
}
140150

@@ -146,13 +156,13 @@ export class ResultCache<A extends CacheableAction, ResultSchema extends AnyZodO
146156
const key = this.cacheKey({ action, keyData })
147157
let cachedValue: JsonObject
148158
try {
149-
cachedValue = await this.cacheStorage.get(key)
159+
cachedValue = await this.cacheStorage.get(key, action)
150160
} catch (e) {
151161
if (!(e instanceof CacheStorageError)) {
152162
throw e
153163
}
154164

155-
action.createLog(log).debug(e.message)
165+
log.verbose(`Error getting results cache entry for key=${key}: ${e.describe()}`)
156166
return undefined
157167
}
158168

@@ -172,13 +182,13 @@ export class ResultCache<A extends CacheableAction, ResultSchema extends AnyZodO
172182

173183
const key = this.cacheKey({ action, keyData })
174184
try {
175-
return await this.cacheStorage.put(key, validatedResult)
185+
return await this.cacheStorage.put(key, validatedResult, action)
176186
} catch (e) {
177187
if (!(e instanceof CacheStorageError)) {
178188
throw e
179189
}
180190

181-
action.createLog(log).debug(e.message)
191+
log.verbose(`Error storing results cache entry for key=${key}: ${e.describe()}`)
182192
return undefined
183193
}
184194
}

0 commit comments

Comments
 (0)