@bluedynamics/cdk8s-plone 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.claude/claude.md +39 -0
  2. package/.claude/settings.local.json +31 -0
  3. package/.jsii +4 -4
  4. package/README.md +15 -2
  5. package/documentation/sources/how-to/deploy-classic-ui.md +322 -0
  6. package/documentation/sources/how-to/deploy-production-volto.md +319 -0
  7. package/documentation/sources/how-to/index.md +13 -0
  8. package/documentation/sources/reference/api/index.md +29 -0
  9. package/examples/classic-ui/.env.example +19 -0
  10. package/examples/classic-ui/README.md +343 -0
  11. package/examples/classic-ui/__snapshots__/main.test.ts.snap +1242 -0
  12. package/examples/classic-ui/cdk8s.yaml +6 -0
  13. package/examples/classic-ui/config/varnish.tpl.vcl +217 -0
  14. package/examples/classic-ui/ingress.ts +217 -0
  15. package/examples/classic-ui/jest.config.js +11 -0
  16. package/examples/classic-ui/main.test.ts +11 -0
  17. package/examples/classic-ui/main.ts +100 -0
  18. package/examples/classic-ui/package-lock.json +5719 -0
  19. package/examples/classic-ui/package.json +36 -0
  20. package/examples/classic-ui/postgres.bitnami.ts +49 -0
  21. package/examples/classic-ui/postgres.cloudnativepg.ts +63 -0
  22. package/examples/production-volto/.env.example +20 -0
  23. package/examples/production-volto/README.md +295 -0
  24. package/examples/production-volto/__snapshots__/main.test.ts.snap +1412 -0
  25. package/examples/production-volto/cdk8s.yaml +6 -0
  26. package/examples/production-volto/config/varnish.tpl.vcl +297 -0
  27. package/examples/production-volto/ingress.ts +229 -0
  28. package/examples/production-volto/jest.config.js +11 -0
  29. package/examples/production-volto/main.test.ts +11 -0
  30. package/examples/production-volto/main.ts +104 -0
  31. package/examples/production-volto/package-lock.json +5714 -0
  32. package/examples/production-volto/package.json +36 -0
  33. package/examples/production-volto/postgres.bitnami.ts +49 -0
  34. package/examples/production-volto/postgres.cloudnativepg.ts +63 -0
  35. package/lib/httpcache.js +1 -1
  36. package/lib/plone.js +1 -1
  37. package/package.json +3 -3
@@ -0,0 +1,6 @@
1
+ language: typescript
2
+ app: npx ts-node main.ts
3
+ imports:
4
+ - k8s
5
+ - https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.24/config/crd/bases/postgresql.cnpg.io_clusters.yaml
6
+ - https://raw.githubusercontent.com/traefik/traefik/v3.2/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml
@@ -0,0 +1,217 @@
1
+ vcl 4.0;
2
+
3
+ import std;
4
+ import directors;
5
+
6
+ probe ploneBackendProbe {
7
+ .url = "/";
8
+ .timeout = 5s;
9
+ .interval = 15s;
10
+ .window = 10;
11
+ .threshold = 8;
12
+ }
13
+
14
+ backend ploneBackend {
15
+ .host = "{{ .Env.BACKEND_SERVICE_NAME }}";
16
+ .port = "{{ .Env.BACKEND_SERVICE_PORT }}";
17
+ .probe = ploneBackendProbe;
18
+ .connect_timeout = 0.5s;
19
+ .first_byte_timeout = 120s;
20
+ .between_bytes_timeout = 60s;
21
+ }
22
+
23
+ /* Only allow PURGE from kubernetes network */
24
+ acl purge {
25
+ "10.0.0.0/8";
26
+ }
27
+
28
+ sub detect_debug{
29
+ # Requests with X-Varnish-Debug will display additional
30
+ # information about requests
31
+ unset req.http.x-vcl-debug;
32
+ set req.http.x-vcl-debug = true;
33
+ }
34
+
35
+ sub detect_auth{
36
+ unset req.http.x-auth;
37
+ if (
38
+ (req.http.Cookie && (
39
+ req.http.Cookie ~ "__ac(_(name|password|persistent))?=" ||
40
+ req.http.Cookie ~ "_ZopeId" ||
41
+ req.http.Cookie ~ "auth_token")) ||
42
+ (req.http.Authenticate) ||
43
+ (req.http.Authorization)
44
+ ) {
45
+ set req.http.x-auth = true;
46
+ }
47
+ }
48
+
49
+ sub vcl_init {
50
+ new lbPloneBackend = directors.round_robin();
51
+ lbPloneBackend.add_backend(ploneBackend);
52
+ }
53
+
54
+ sub vcl_recv {
55
+ # Annotate request with x-vcl-debug
56
+ call detect_debug;
57
+
58
+ # Annotate request with x-auth indicating if request is authenticated or not
59
+ call detect_auth;
60
+
61
+ # Routing: All traffic goes to Plone backend for Classic UI
62
+ set req.backend_hint = lbPloneBackend.backend();
63
+
64
+ # short cut authenticated requests to pass
65
+ if (req.http.x-auth) {
66
+ return(pass);
67
+ }
68
+
69
+ # Sanitize cookies so they do not needlessly destroy cacheability for anonymous pages
70
+ if (req.http.Cookie) {
71
+ set req.http.Cookie = ";" + req.http.Cookie;
72
+ set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";");
73
+ set req.http.Cookie = regsuball(req.http.Cookie, ";(sticky|I18N_LANGUAGE|statusmessages|__ac|_ZopeId|__cp|beaker\.session|authomatic|serverid|__rf|auth_token)=", "; \1=");
74
+ set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", "");
75
+ set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", "");
76
+
77
+ if (req.http.Cookie == "") {
78
+ unset req.http.Cookie;
79
+ }
80
+ }
81
+
82
+ # Handle the different request types
83
+ if (req.method == "PURGE") {
84
+ if (!client.ip ~ purge) {
85
+ return (synth(405, "Not allowed."));
86
+ } else {
87
+ ban("req.url == " + req.url);
88
+ return (synth(200, "Purged."));
89
+ }
90
+
91
+ } elseif (req.method == "BAN") {
92
+ # Same ACL check as above:
93
+ if (!client.ip ~ purge) {
94
+ return (synth(405, "Not allowed."));
95
+ }
96
+ ban("req.http.host == " + req.http.host + "&& req.url == " + req.url);
97
+ # Throw a synthetic page so the
98
+ # request won't go to the backend.
99
+ return (synth(200, "Ban added"));
100
+
101
+ } elseif (req.method != "GET" &&
102
+ req.method != "HEAD" &&
103
+ req.method != "PUT" &&
104
+ req.method != "POST" &&
105
+ req.method != "PATCH" &&
106
+ req.method != "TRACE" &&
107
+ req.method != "OPTIONS" &&
108
+ req.method != "DELETE") {
109
+ /* Non-RFC2616 or CONNECT which is weird. */
110
+ return (pipe);
111
+ } elseif (req.method != "GET" &&
112
+ req.method != "HEAD" &&
113
+ req.method != "OPTIONS") {
114
+ /* POST, PUT, PATCH will pass, always */
115
+ return(pass);
116
+ }
117
+
118
+ return(hash);
119
+ }
120
+
121
+ sub vcl_pipe {
122
+ /* This is not necessary if you do not do any request rewriting. */
123
+ set req.http.connection = "close";
124
+ }
125
+
126
+ sub vcl_purge {
127
+ return (synth(200, "PURGE: " + req.url + " - " + req.hash));
128
+ }
129
+
130
+ sub vcl_synth {
131
+ if (resp.status == 301) {
132
+ set resp.http.location = resp.reason;
133
+ set resp.reason = "Moved";
134
+ return (deliver);
135
+ }
136
+ }
137
+
138
+ sub vcl_hit {
139
+ if (obj.ttl >= 0s) {
140
+ // A pure unadulterated hit, deliver it
141
+ return (deliver);
142
+ } elsif (obj.ttl + obj.grace > 0s) {
143
+ // Object is in grace, deliver it
144
+ // Automatically triggers a background fetch
145
+ return (deliver);
146
+ } else {
147
+ return (restart);
148
+ }
149
+ }
150
+
151
+
152
+ sub vcl_backend_response {
153
+ # Don't allow static files to set cookies.
154
+ # (?i) denotes case insensitive in PCRE (perl compatible regular expressions).
155
+ if (bereq.url ~ "(?i)\.(pdf|asc|dat|txt|doc|xls|ppt|tgz|png|gif|jpeg|jpg|ico|swf|css|js)(\?.*)?$") {
156
+ unset beresp.http.set-cookie;
157
+ }
158
+ if (beresp.http.Set-Cookie) {
159
+ set beresp.http.x-varnish-action = "FETCH (pass - response sets cookie)";
160
+ set beresp.uncacheable = true;
161
+ set beresp.ttl = 120s;
162
+ return(deliver);
163
+ }
164
+ if (beresp.http.Cache-Control ~ "(private|no-cache|no-store)") {
165
+ set beresp.http.x-varnish-action = "FETCH (pass - cache control disallows)";
166
+ set beresp.uncacheable = true;
167
+ set beresp.ttl = 120s;
168
+ return(deliver);
169
+ }
170
+
171
+ if (beresp.http.Authorization) {
172
+ set beresp.http.x-varnish-action = "FETCH (pass - authorized and no public cache control)";
173
+ set beresp.uncacheable = true;
174
+ set beresp.ttl = 120s;
175
+ return(deliver);
176
+ }
177
+
178
+ if (!beresp.http.Cache-Control) {
179
+ set beresp.http.x-varnish-action = "FETCH (override - backend not setting cache control)";
180
+ set beresp.uncacheable = true;
181
+ set beresp.ttl = 120s;
182
+ return (deliver);
183
+ }
184
+
185
+ if (beresp.http.X-Anonymous && !beresp.http.Cache-Control) {
186
+ set beresp.http.x-varnish-action = "FETCH (override - anonymous backend not setting cache control)";
187
+ set beresp.ttl = 600s;
188
+ return (deliver);
189
+ }
190
+
191
+ set beresp.http.x-varnish-action = "FETCH (insert)";
192
+ return (deliver);
193
+ }
194
+
195
+ sub vcl_deliver {
196
+
197
+ if (req.http.x-vcl-debug) {
198
+ set resp.http.x-varnish-ttl = obj.ttl;
199
+ set resp.http.x-varnish-grace = obj.grace;
200
+ set resp.http.x-hits = obj.hits;
201
+ if (req.http.x-auth) {
202
+ set resp.http.x-auth = "Logged-in";
203
+ } else {
204
+ set resp.http.x-auth = "Anon";
205
+ }
206
+ if (obj.hits > 0) {
207
+ set resp.http.x-cache = "HIT";
208
+ } else {
209
+ set resp.http.x-cache = "MISS";
210
+ }
211
+ } else {
212
+ unset resp.http.x-varnish-action;
213
+ unset resp.http.x-cache-operation;
214
+ unset resp.http.x-cache-rule;
215
+ unset resp.http.x-powered-by;
216
+ }
217
+ }
@@ -0,0 +1,217 @@
1
+ import { Construct } from 'constructs';
2
+ import * as k8s from './imports/k8s';
3
+ import * as traefik from './imports/traefik.io';
4
+
5
+ export interface IngressOptions {
6
+
7
+ /**
8
+ * type: kong or traefik
9
+ * @default - traefik
10
+ */
11
+ readonly ingressType?: string;
12
+
13
+ /**
14
+ * issuer: cert-manager/cluster-issuer
15
+ * @default none
16
+ */
17
+ readonly issuer: string;
18
+
19
+ /**
20
+ * domainCached is the Domain used for the cached setup and should be the main public domain
21
+ * @default - none
22
+ */
23
+ readonly domainCached: string;
24
+
25
+ /**
26
+ * domainUncached is the Domain used for the uncached setup for testing purposes only
27
+ * @default - none
28
+ */
29
+ readonly domainUncached: string;
30
+
31
+ /**
32
+ * domainMaintenance is the Domain to access the Plone backend (API) server for maintenance
33
+ * @default - none
34
+ */
35
+
36
+ readonly domainMaintenance: string;
37
+
38
+ /**
39
+ * backendServiceName is the K8S Service name of the Plone backend
40
+ * @default - none
41
+ */
42
+ readonly backendServiceName: string;
43
+
44
+ /**
45
+ * httpcacheServiceName is the K8S Service name of the http-cache (Varnish)
46
+ * @default - none
47
+ */
48
+ readonly httpcacheServiceName: string;
49
+ }
50
+
51
+
52
+ export class IngressChart extends Construct {
53
+
54
+ readonly issuer: string;
55
+
56
+ constructor(scope: Construct, id: string, options: IngressOptions) {
57
+ super(scope, id);
58
+ this.issuer = options.issuer;
59
+ if (options.ingressType === 'traefik') {
60
+ // Cached domain routes through Varnish
61
+ this.traefikIngress(
62
+ 'main',
63
+ 'cached',
64
+ options.domainCached,
65
+ '/',
66
+ options.httpcacheServiceName,
67
+ 80,
68
+ );
69
+ // Uncached domain goes direct to backend
70
+ this.traefikIngress(
71
+ 'main',
72
+ 'uncached',
73
+ options.domainUncached,
74
+ `/`,
75
+ options.backendServiceName,
76
+ 8080,
77
+ `/VirtualHostBase/https/${options.domainUncached}/Plone/VirtualHostRoot/`
78
+ );
79
+ // Maintenance domain for backend access
80
+ this.traefikIngress(
81
+ 'main',
82
+ 'maintenance',
83
+ options.domainMaintenance,
84
+ `/`,
85
+ options.backendServiceName,
86
+ 8080,
87
+ `/VirtualHostBase/https/${options.domainMaintenance}/VirtualHostRoot/`
88
+ );
89
+ } else if (options.ingressType === 'kong') {
90
+
91
+ // Create the ingress for the cached (main) domain
92
+ this.kongIngress(
93
+ 'main',
94
+ 'cached',
95
+ options.domainCached,
96
+ '/',
97
+ options.httpcacheServiceName,
98
+ 80,
99
+ );
100
+
101
+ // Create the ingress for the uncached (test) domain - direct to backend
102
+ this.kongIngress(
103
+ 'uncached',
104
+ 'backend',
105
+ options.domainUncached,
106
+ '/~/(.*)',
107
+ options.backendServiceName,
108
+ 8080,
109
+ `/VirtualHostBase/https/${options.domainUncached}/Plone/VirtualHostRoot/$1`,
110
+ );
111
+
112
+ // Create the ingress for the maintenance
113
+ this.kongIngress(
114
+ 'maintenance',
115
+ 'backend',
116
+ options.domainMaintenance,
117
+ '/~/(.*)',
118
+ options.backendServiceName,
119
+ 8080,
120
+ `/VirtualHostBase/https/${options.domainMaintenance}/VirtualHostRoot/$1`
121
+ );
122
+ } else {
123
+ throw new Error('Unknown ingress type');
124
+ }
125
+ }
126
+
127
+ traefikIngress(prefix: string, postfix: string, domain: string, path: string, backendServiceName: string, backendPort: number, rewrite?: string) {
128
+ var annotations: { [key: string]: string } = {
129
+ 'kubernetes.io/ingress.class': 'traefik',
130
+ 'cert-manager.io/cluster-issuer': this.issuer,
131
+ };
132
+ if (rewrite !== undefined) {
133
+ const rewritemw = new traefik.Middleware(this, `${prefix}-${postfix}-addprefix`,
134
+ {
135
+ metadata: {},
136
+ spec: {
137
+ addPrefix: {
138
+ prefix: rewrite,
139
+ },
140
+ },
141
+ }
142
+ );
143
+ annotations['traefik.ingress.kubernetes.io/router.middlewares'] = `plone-${rewritemw.name}@kubernetescrd`;
144
+ }
145
+
146
+ new k8s.KubeIngress(this, `${prefix}-${postfix}`, {
147
+ metadata: {
148
+ annotations: annotations,
149
+ },
150
+ spec: {
151
+ ingressClassName: 'traefik',
152
+ rules: [{
153
+ host: domain,
154
+ http: {
155
+ paths: [{
156
+ backend: {
157
+ service: {
158
+ name: backendServiceName,
159
+ port: { number: backendPort },
160
+ },
161
+ },
162
+ path: path,
163
+ pathType: 'Prefix',
164
+ }],
165
+ },
166
+ }],
167
+ },
168
+ });
169
+
170
+ }
171
+
172
+ kongIngress(prefix: string, postfix: string, domain: string, path: string, backendServiceName: string, backendPort: number, rewrite?: string) {
173
+ /*
174
+ Create a kong general ingress
175
+
176
+ Properties:
177
+ - prefix: prefix for the ingress name used for grouping tls secrets
178
+ - postfix: postfix for the ingress name, used concatenated with the prefix as identifer for the ingress
179
+ - domain: domain for the ingress
180
+ - path: path for the ingress
181
+ - backendServiceName: name of the backend service
182
+ - backendPort: port of the backend service
183
+ - rewrite (optional): rewrite path for the ingress
184
+ */
185
+ var annotations: { [key: string]: string } = {
186
+ 'cert-manager.io/cluster-issuer': 'sectigo-issuer',
187
+ 'konghq.com/https-redirect-status-code': '308',
188
+ 'konghq.com/protocols': 'https',
189
+ };
190
+ if (rewrite !== undefined) {
191
+ annotations['konghq.com/rewrite'] = rewrite;
192
+ }
193
+ new k8s.KubeIngress(this, `${prefix}-${postfix}`, {
194
+ metadata: {
195
+ annotations: annotations,
196
+ },
197
+ spec: {
198
+ ingressClassName: 'kong',
199
+ rules: [{
200
+ host: domain,
201
+ http: {
202
+ paths: [{
203
+ backend: {
204
+ service: {
205
+ name: backendServiceName,
206
+ port: { number: backendPort },
207
+ },
208
+ },
209
+ path: path,
210
+ pathType: 'Prefix',
211
+ }],
212
+ },
213
+ }],
214
+ },
215
+ });
216
+ }
217
+ }
@@ -0,0 +1,11 @@
1
+ module.exports = {
2
+ roots: [
3
+ "<rootDir>"
4
+ ],
5
+ moduleFileExtensions: ["ts", "tsx", "js", "mjs", "cjs", "jsx", "json", "node"],
6
+ testMatch: ['**/*.test.ts'],
7
+ transform: {
8
+ "^.+\\.tsx?$": "ts-jest"
9
+ },
10
+ testEnvironment: 'node',
11
+ }
@@ -0,0 +1,11 @@
1
+ import { ClassicUIChart } from './main';
2
+ import { Testing } from 'cdk8s';
3
+
4
+ describe('Classic UI Example', () => {
5
+ test('Synthesizes correctly', () => {
6
+ const app = Testing.app();
7
+ const chart = new ClassicUIChart(app, 'test-chart');
8
+ const results = Testing.synth(chart);
9
+ expect(results).toMatchSnapshot();
10
+ });
11
+ });
@@ -0,0 +1,100 @@
1
+ import { Construct } from 'constructs';
2
+ import { App, Chart, ChartProps } from 'cdk8s';
3
+ import { Plone, PloneVariant, PloneHttpcache } from '@bluedynamics/cdk8s-plone';
4
+ import * as kplus from 'cdk8s-plus-30';
5
+ import * as path from 'path';
6
+ import { IngressChart } from './ingress';
7
+ import { config } from 'dotenv';
8
+ import { PGBitnamiChart } from './postgres.bitnami';
9
+ import { PGCloudNativePGChart } from './postgres.cloudnativepg';
10
+
11
+
12
+ export class ClassicUIChart extends Chart {
13
+ constructor(scope: Construct, id: string, props: ChartProps = {}) {
14
+ super(scope, id, props);
15
+
16
+ config();
17
+
18
+ // ================================================================================================================
19
+ // Postgresql
20
+ let db: PGBitnamiChart | PGCloudNativePGChart;
21
+ let postgresql_username;
22
+ let postgresql_password;
23
+ if ((process.env.DATABASE ?? 'bitnami') == 'cloudnativepg') {
24
+ const cloudnativepgDb = new PGCloudNativePGChart(this, 'db');
25
+ db = cloudnativepgDb;
26
+ // CloudNativePG creates secrets with format: {cluster-name}-app
27
+ // Use the CDK8S-generated cluster name, never hard-code
28
+ const secretName = `${cloudnativepgDb.clusterName}-app`;
29
+ postgresql_username = { valueFrom: { secretKeyRef: { name: secretName, key: 'username' }}};
30
+ postgresql_password = { valueFrom: { secretKeyRef: { name: secretName, key: 'password' }}};
31
+ } else {
32
+ const bitnamiDb = new PGBitnamiChart(this, 'db');
33
+ db = bitnamiDb;
34
+ postgresql_username = { value: 'plone' };
35
+ postgresql_password = { valueFrom: { secretKeyRef: { name: `${bitnamiDb.dbServiceName}`, key: 'password' }}};
36
+ }
37
+
38
+ // ================================================================================================================
39
+ // Plone Classic UI
40
+
41
+ // prepare the environment variables for the plone deployment
42
+ const dbMDName = db.dbServiceName
43
+ const env = new kplus.Env(
44
+ [],
45
+ {
46
+ SECRET_POSTGRESQL_USERNAME: postgresql_username,
47
+ SECRET_POSTGRESQL_PASSWORD: postgresql_password,
48
+ INSTANCE_db_storage: { value: `relstorage` },
49
+ INSTANCE_db_blob_mode: { value: `cache` },
50
+ INSTANCE_db_cache_size: { value: `5000` },
51
+ INSTANCE_db_cache_size_bytes: { value: `1500MB` },
52
+ INSTANCE_db_relstorage: { value: `postgresql` },
53
+ INSTANCE_db_relstorage_postgresql_dsn: { value: `host='${dbMDName}' dbname='plone' user='$(SECRET_POSTGRESQL_USERNAME)' password='$(SECRET_POSTGRESQL_PASSWORD)'` },
54
+ INSTANCE_db_relstorage_cache_local_mb: { value: `800` },
55
+ },
56
+ );
57
+
58
+ // create the plone deployment with Classic UI variant
59
+ const plone = new Plone(this, 'plone', {
60
+ version: 'classic.version',
61
+ variant: PloneVariant.CLASSICUI,
62
+ backend: {
63
+ image: process.env.PLONE_BACKEND_IMAGE ?? 'plone/plone-backend:6.1.3',
64
+ environment: env,
65
+ replicas: 2,
66
+ },
67
+ })
68
+
69
+ // ================================================================================================================
70
+ // Varnish with kube-httpcache
71
+ const httpcache = new PloneHttpcache(
72
+ this,
73
+ 'httpcache',
74
+ {
75
+ plone: plone,
76
+ varnishVclFile: path.join(__dirname, 'config', 'varnish.tpl.vcl'),
77
+ }
78
+ )
79
+
80
+ // ================================================================================================================
81
+ // Ingress
82
+ new IngressChart(
83
+ this,
84
+ 'ingress',
85
+ {
86
+ ingressType: 'traefik',
87
+ issuer: process.env.CLUSTER_ISSUER ?? 'letsencrypt-prod',
88
+ domainCached: process.env.DOMAIN_CACHED ?? 'plone-cached.example.com',
89
+ domainUncached: process.env.DOMAIN_UNCACHED ?? 'plone-uncached.example.com',
90
+ domainMaintenance: process.env.DOMAIN_MAINTENANCE ?? 'plone-maintenance.example.com',
91
+ backendServiceName: plone.backendServiceName,
92
+ httpcacheServiceName: httpcache.httpcacheServiceName,
93
+ });
94
+ }
95
+ }
96
+
97
+
98
+ const app = new App();
99
+ new ClassicUIChart(app, 'plone-classic');
100
+ app.synth();