mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 15:59:41 +02:00
fix(caching): integrate with axios cache interceptor [EE-6505] (#10922)
* integrate with axios-cache-interceptor * remove extra headers as not needed
This commit is contained in:
parent
d0b9e3a732
commit
0b9cebc685
8 changed files with 123 additions and 55 deletions
|
@ -107,6 +107,7 @@ func kubeOnlyMiddleware(next http.Handler) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rw.Header().Set(portainer.PortainerCacheHeader, "true")
|
||||||
next.ServeHTTP(rw, request)
|
next.ServeHTTP(rw, request)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,5 +57,11 @@ func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Respons
|
||||||
request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey())
|
request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey())
|
||||||
request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
|
request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
|
||||||
|
|
||||||
return transport.baseTransport.RoundTrip(request)
|
response, err := transport.baseTransport.RoundTrip(request)
|
||||||
|
if err != nil {
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
response.Header.Set(portainer.PortainerCacheHeader, "true")
|
||||||
|
|
||||||
|
return response, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1638,6 +1638,8 @@ const (
|
||||||
WebSocketKeepAlive = 1 * time.Hour
|
WebSocketKeepAlive = 1 * time.Hour
|
||||||
// AuthCookieName is the name of the cookie used to store the JWT token
|
// AuthCookieName is the name of the cookie used to store the JWT token
|
||||||
AuthCookieKey = "portainer_api_key"
|
AuthCookieKey = "portainer_api_key"
|
||||||
|
// PortainerCacheHeader is used to enabled FE caching for Kubernetes resources
|
||||||
|
PortainerCacheHeader = "X-Portainer-Cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
// List of supported features
|
// List of supported features
|
||||||
|
|
|
@ -1,8 +1,18 @@
|
||||||
import axiosOrigin, { AxiosError, InternalAxiosRequestConfig } from 'axios';
|
import Axios, {
|
||||||
import { setupCache } from 'axios-cache-adapter';
|
AxiosError,
|
||||||
|
AxiosInstance,
|
||||||
|
InternalAxiosRequestConfig,
|
||||||
|
} from 'axios';
|
||||||
|
import {
|
||||||
|
setupCache,
|
||||||
|
buildMemoryStorage,
|
||||||
|
CacheAxiosResponse,
|
||||||
|
InterpreterResult,
|
||||||
|
AxiosCacheInstance,
|
||||||
|
} from 'axios-cache-interceptor';
|
||||||
import { loadProgressBar } from 'axios-progress-bar';
|
import { loadProgressBar } from 'axios-progress-bar';
|
||||||
|
|
||||||
import 'axios-progress-bar/dist/nprogress.css';
|
import 'axios-progress-bar/dist/nprogress.css';
|
||||||
|
|
||||||
import PortainerError from '@/portainer/error';
|
import PortainerError from '@/portainer/error';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -12,55 +22,82 @@ import {
|
||||||
portainerAgentTargetHeader,
|
portainerAgentTargetHeader,
|
||||||
} from './http-request.helper';
|
} from './http-request.helper';
|
||||||
|
|
||||||
export const cache = setupCache({
|
const portainerCacheHeader = 'X-Portainer-Cache';
|
||||||
maxAge: CACHE_DURATION,
|
|
||||||
debug: false, // set to true to print cache hits/misses
|
|
||||||
exclude: {
|
|
||||||
query: false, // include urls with query params
|
|
||||||
methods: ['put', 'patch', 'delete'],
|
|
||||||
filter: (req: InternalAxiosRequestConfig) => {
|
|
||||||
// exclude caching get requests unless the path contains 'kubernetes'
|
|
||||||
if (!req.url?.includes('kubernetes') && req.method === 'get') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const acceptHeader = req.headers?.Accept;
|
const storage = buildMemoryStorage();
|
||||||
if (
|
// mock the cache adapter
|
||||||
acceptHeader &&
|
export const cache = {
|
||||||
typeof acceptHeader === 'string' &&
|
store: {
|
||||||
acceptHeader.includes('application/yaml')
|
clear: () => {
|
||||||
) {
|
storage.data = {};
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// exclude caching post requests unless the path contains 'selfsubjectaccessreview'
|
|
||||||
if (
|
|
||||||
!req.url?.includes('selfsubjectaccessreview') &&
|
|
||||||
req.method === 'post'
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// ask to clear cache on mutation
|
};
|
||||||
invalidate: async (_, req) => {
|
|
||||||
dispatchCacheRefreshEventIfNeeded(req);
|
function headerInterpreter(
|
||||||
},
|
headers?: CacheAxiosResponse['headers']
|
||||||
|
): InterpreterResult {
|
||||||
|
if (!headers) {
|
||||||
|
return 'not enough headers';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headers[portainerCacheHeader]) {
|
||||||
|
return CACHE_DURATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'not enough headers';
|
||||||
|
}
|
||||||
|
|
||||||
|
const axios = Axios.create({ baseURL: 'api' });
|
||||||
|
axios.interceptors.request.use((req) => {
|
||||||
|
dispatchCacheRefreshEventIfNeeded(req);
|
||||||
|
return req;
|
||||||
});
|
});
|
||||||
|
|
||||||
// by default don't use the cache adapter
|
// type guard the axios instance
|
||||||
const axios = axiosOrigin.create({ baseURL: 'api' });
|
function isAxiosCacheInstance(
|
||||||
|
a: AxiosInstance | AxiosCacheInstance
|
||||||
|
): a is AxiosCacheInstance {
|
||||||
|
return (a as AxiosCacheInstance).defaults.cache !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// when entering a kubernetes environment, or updating user settings, update the cache adapter
|
// when entering a kubernetes environment, or updating user settings, update the cache adapter
|
||||||
export function updateAxiosAdapter(useCache: boolean) {
|
export function updateAxiosAdapter(useCache: boolean) {
|
||||||
axios.defaults.adapter = useCache ? cache.adapter : undefined;
|
if (useCache) {
|
||||||
|
if (isAxiosCacheInstance(axios)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupCache(axios, {
|
||||||
|
storage,
|
||||||
|
ttl: CACHE_DURATION,
|
||||||
|
methods: ['get', 'head', 'options', 'post'],
|
||||||
|
// cachePredicate determines if the response should be cached based on response
|
||||||
|
cachePredicate: {
|
||||||
|
containsHeaders: {
|
||||||
|
[portainerCacheHeader]: () => true,
|
||||||
|
},
|
||||||
|
ignoreUrls: [/^(?!.*\bkubernetes\b).*$/gm],
|
||||||
|
responseMatch: (res) => {
|
||||||
|
if (res.config.method === 'post') {
|
||||||
|
if (res.config.url?.includes('selfsubjectaccessreviews')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// headerInterpreter interprets the response headers to determine if the response should be cached
|
||||||
|
headerInterpreter,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadProgressBar(undefined, axios);
|
|
||||||
|
|
||||||
export default axios;
|
export default axios;
|
||||||
|
|
||||||
|
loadProgressBar(undefined, axios);
|
||||||
|
|
||||||
export const agentTargetHeader = 'X-PortainerAgent-Target';
|
export const agentTargetHeader = 'X-PortainerAgent-Target';
|
||||||
|
|
||||||
export function agentInterceptor(config: InternalAxiosRequestConfig) {
|
export function agentInterceptor(config: InternalAxiosRequestConfig) {
|
||||||
|
@ -173,7 +210,7 @@ export function isDefaultResponse(
|
||||||
export function isAxiosError<ResponseType>(
|
export function isAxiosError<ResponseType>(
|
||||||
error: unknown
|
error: unknown
|
||||||
): error is AxiosError<ResponseType> {
|
): error is AxiosError<ResponseType> {
|
||||||
return axiosOrigin.isAxiosError(error);
|
return Axios.isAxiosError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function arrayToJson<T>(arr?: Array<T>) {
|
export function arrayToJson<T>(arr?: Array<T>) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { InternalAxiosRequestConfig, AxiosResponse } from 'axios';
|
import { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||||
|
import { CacheAxiosResponse } from 'axios-cache-interceptor';
|
||||||
import { IHttpResponse } from 'angular';
|
import { IHttpResponse } from 'angular';
|
||||||
|
|
||||||
import axios from './axios';
|
import axios from './axios';
|
||||||
|
@ -8,7 +9,9 @@ axios.interceptors.request.use(csrfInterceptor);
|
||||||
|
|
||||||
let csrfToken: string | null = null;
|
let csrfToken: string | null = null;
|
||||||
|
|
||||||
export function csrfTokenReaderInterceptor(config: AxiosResponse) {
|
export function csrfTokenReaderInterceptor(
|
||||||
|
config: CacheAxiosResponse | AxiosResponse
|
||||||
|
) {
|
||||||
const csrfTokenHeader = config.headers['x-csrf-token'];
|
const csrfTokenHeader = config.headers['x-csrf-token'];
|
||||||
if (csrfTokenHeader) {
|
if (csrfTokenHeader) {
|
||||||
csrfToken = csrfTokenHeader;
|
csrfToken = csrfTokenHeader;
|
||||||
|
|
|
@ -216,6 +216,14 @@ async function getApplicationByKind<
|
||||||
buildUrl(environmentId, namespace, `${appKind}s`, name),
|
buildUrl(environmentId, namespace, `${appKind}s`, name),
|
||||||
{
|
{
|
||||||
headers: { Accept: yaml ? 'application/yaml' : 'application/json' },
|
headers: { Accept: yaml ? 'application/yaml' : 'application/json' },
|
||||||
|
// this logic is to get the latest YAML response
|
||||||
|
// axios-cache-adapter looks for the response headers to determine if the response should be cached
|
||||||
|
// to avoid writing requestInterceptor, adding a query param to the request url
|
||||||
|
params: yaml
|
||||||
|
? {
|
||||||
|
_: Date.now(),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
|
|
|
@ -74,7 +74,7 @@
|
||||||
"angularjs-slider": "^6.4.0",
|
"angularjs-slider": "^6.4.0",
|
||||||
"angulartics": "^1.6.0",
|
"angulartics": "^1.6.0",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"axios-cache-adapter": "^2.7.3",
|
"axios-cache-interceptor": "^1.4.1",
|
||||||
"axios-progress-bar": "^1.2.0",
|
"axios-progress-bar": "^1.2.0",
|
||||||
"babel-plugin-angularjs-annotate": "^0.10.0",
|
"babel-plugin-angularjs-annotate": "^0.10.0",
|
||||||
"bootstrap": "^3.4.0",
|
"bootstrap": "^3.4.0",
|
||||||
|
|
31
yarn.lock
31
yarn.lock
|
@ -7231,13 +7231,14 @@ axe-core@^4.6.2:
|
||||||
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf"
|
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf"
|
||||||
integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==
|
integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==
|
||||||
|
|
||||||
axios-cache-adapter@^2.7.3:
|
axios-cache-interceptor@^1.4.1:
|
||||||
version "2.7.3"
|
version "1.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/axios-cache-adapter/-/axios-cache-adapter-2.7.3.tgz#0d1eefa0f25b88f42a95c7528d7345bde688181d"
|
resolved "https://registry.yarnpkg.com/axios-cache-interceptor/-/axios-cache-interceptor-1.4.1.tgz#1046a9e77410d7405b03d470c74bd074629258f5"
|
||||||
integrity sha512-A+ZKJ9lhpjthOEp4Z3QR/a9xC4du1ALaAsejgRGrH9ef6kSDxdFrhRpulqsh9khsEnwXxGfgpUuDp1YXMNMEiQ==
|
integrity sha512-Ax4+PiGfNxpQvyF00t55nFzWoVnqW7slKCg9va6dbqiuAGIxRE8r1uMzunw8TKJ5iwLivFqAb0EeiLeUCxuZIw==
|
||||||
dependencies:
|
dependencies:
|
||||||
cache-control-esm "1.0.0"
|
cache-parser "1.2.4"
|
||||||
md5 "^2.2.1"
|
fast-defer "1.1.8"
|
||||||
|
object-code "1.3.2"
|
||||||
|
|
||||||
axios-progress-bar@^1.2.0:
|
axios-progress-bar@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
|
@ -7781,10 +7782,10 @@ bytes@3.1.2:
|
||||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
|
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
|
||||||
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
|
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
|
||||||
|
|
||||||
cache-control-esm@1.0.0:
|
cache-parser@1.2.4:
|
||||||
version "1.0.0"
|
version "1.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/cache-control-esm/-/cache-control-esm-1.0.0.tgz#417647ecf1837a5e74155f55d5a4ae32a84e2581"
|
resolved "https://registry.yarnpkg.com/cache-parser/-/cache-parser-1.2.4.tgz#60975135ef2330e6a1d60895279d7237a2a9b398"
|
||||||
integrity sha512-Fa3UV4+eIk4EOih8FTV6EEsVKO0W5XWtNs6FC3InTfVz+EjurjPfDXY5wZDo/lxjDxg5RjNcurLyxEJBcEUx9g==
|
integrity sha512-O0KwuHuJnbHUrghHi2kGp0SxnWSIBXTYt7M8WVhW0kbPRUNUKoE/Of6e1rRD6AAxmfxFunKnt90yEK09D+sc5g==
|
||||||
|
|
||||||
call-bind@^1.0.0, call-bind@^1.0.2:
|
call-bind@^1.0.0, call-bind@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
|
@ -10068,6 +10069,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||||
|
|
||||||
|
fast-defer@1.1.8:
|
||||||
|
version "1.1.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/fast-defer/-/fast-defer-1.1.8.tgz#940ef9597b2ea51c4cd08e99d0f2a8978fa49ba2"
|
||||||
|
integrity sha512-lEJeOH5VL5R09j6AA0D4Uvq7AgsHw0dAImQQ+F3iSyHZuAxyQfWobsagGpTcOPvJr3urmKRHrs+Gs9hV+/Qm/Q==
|
||||||
|
|
||||||
fast-glob@^3.1.1:
|
fast-glob@^3.1.1:
|
||||||
version "3.2.7"
|
version "3.2.7"
|
||||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1"
|
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1"
|
||||||
|
@ -13598,6 +13604,11 @@ object-assign@^4.0.1, object-assign@^4.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||||
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
|
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
|
||||||
|
|
||||||
|
object-code@1.3.2:
|
||||||
|
version "1.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/object-code/-/object-code-1.3.2.tgz#f0c0fd71b16aed45a58c306b831f2249806dffd5"
|
||||||
|
integrity sha512-3CVDmQiru7YYVr+4OpCGrqkVE7GogmWinDcgfno1fXlKgIhtW9FhgHTiV98gMPUjQwWuWvijQDCY8sGnqKrhTw==
|
||||||
|
|
||||||
object-hash@^3.0.0:
|
object-hash@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"
|
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue