├── .gitignore ├── LICENSE.md ├── README.md ├── database ├── connection.js └── models.js ├── dns ├── .npmrc ├── Dockerfile ├── db.js ├── index.js └── package.json ├── examples └── hello-world │ ├── configMap.json │ ├── deployment.json │ ├── ingress.json │ ├── namespace.json │ ├── pod.json │ ├── secret.json │ └── service.json ├── functions.js ├── index.js ├── loadBalancer ├── .npmrc ├── Dockerfile ├── index.js └── package.json ├── middleware ├── general.js ├── index.js └── openapi.js ├── objects ├── certificateSigningRequest.js ├── clusterRole.js ├── clusterRoleBinding.js ├── configMap.js ├── deployment.js ├── endpoints.js ├── index.js ├── ingress.js ├── namespace.js ├── object.js ├── pod.js ├── role.js ├── roleBinding.js ├── secret.js ├── service.js └── status.js ├── openApiSpecs └── v3 │ ├── api.json │ ├── api │ └── v1.json │ └── apis │ ├── apps │ └── v1.json │ ├── certificates.k8s.io │ └── v1.json │ ├── networking.k8s.io │ └── v1.json │ └── rbac.authorization.k8s.io │ └── v1.json ├── package-lock.json ├── package.json ├── pod_spec.txt ├── routes ├── api.js ├── certificateSigningRequest.js ├── clusterRole.js ├── clusterRoleBinding.js ├── configMap.js ├── deployment.js ├── endpoints.js ├── index.js ├── ingress.js ├── namespace.js ├── namespaceCheck.js ├── openapi.js ├── pod.js ├── role.js ├── roleBinding.js ├── secret.js └── service.js └── start.sh /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2024 Seth Wheeler 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes in NodeJS 2 | 3 | This project was/is an attempt to recreate the core functionality of [v1.29.1 Kubernetes](https://v1-29.docs.kubernetes.io/) in NodeJS, while being fully compatible with the [`kubectl` CLI](https://kubernetes.io/docs/reference/kubectl/). Though this project only implements some resources, the resources which were partially/fully implemented seem to be the most used (i.e. Pods, Services, etc) and the others may be implemented in the future. 4 | 5 | ### Resources Partially Implemented 6 | * Pod (volume mounts are not supported) 7 | * ClusterRole 8 | * ClusterRoleBinding 9 | * Ingress 10 | * Role 11 | * RoleBinding 12 | * Endpoints (untested) 13 | * CertificateSigningRequest 14 | 15 | ### Resources Fully Implemented 16 | * ConfigMap 17 | * Secret 18 | * Deployment 19 | * Namespace 20 | * Service 21 | 22 | ## Requirements 23 | * [NodeJS v20](https://nodejs.org/dist/v20.0.0/) or higher 24 | * [Docker Engine v25.0.3](https://docs.docker.com/engine/install/) or higher 25 | 26 | ###### It is also recommended you install [kubectl](https://kubernetes.io/docs/tasks/tools/), though it is not required 27 | 28 | ## How to use 29 | 1. Install NodeJS and Docker 30 | 2. Install the project dependencies by running `npm i` 31 | 3. If you have your own MongDB instance, create a `.env` file and set `DB_URL` to your `MongoDB` instance. If you do not have a `MongoDB` instance, a `MongoDB` instance will be spun up with Docker for you when you run the project (in the next step). 32 | 4. Run `npm start` (you may need to allow the start script to run by modifying the permissions of `start.sh` (with `chmod`) 33 | 5. Set your `kubectl` to use this project by running `kubectl config use-context /localhost:8080/admin` in your shell 34 | 6. Create Kubernetes resources with `kubectl` 35 | 36 | Upon running the project you can use the examples in `examples/helloworld` or your own YAML to create resources. 37 | 38 | Feel free to open an issue and/or make a PR if something is broken. 39 | 40 | Licensed under the MIT License, full license is available in `LICENSE.md` 41 | -------------------------------------------------------------------------------- /database/connection.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | function connect(mongoConnectString, logger = console) { 4 | mongoose.connect(mongoConnectString); 5 | const mongoDB = mongoose.connection; 6 | 7 | mongoDB.on('error', (err) => { 8 | logger.error(`MongoDB error: \n${err}`); 9 | throw err; 10 | }); 11 | mongoDB.once('connected', () => { 12 | logger.log('Connected to MongoDB!'); 13 | return true; 14 | }); 15 | } 16 | 17 | function closeConnection() { 18 | const mongoDB = mongoose.connection; 19 | if (mongoDB.readyState === 1) { 20 | mongoDB.close(); 21 | 22 | mongoDB.on('error', (err) => { 23 | logger.emerg(`MongoDB error: ${err}`); 24 | throw err; 25 | }); 26 | if (mongoDB.readyState === 3 || mongoDB.readyState === 0) { 27 | mongoDB.once('disconnected', () => { 28 | logger.log('\n' + 'Disconnected from MongoDB!'); 29 | return true; 30 | }); 31 | } else { 32 | throw (new Error('Unable to disconnect from MongoDB')); 33 | } 34 | } 35 | } 36 | 37 | function getConnectionStatus() { 38 | return mongoose.connection.readyState; 39 | } 40 | 41 | module.exports = { 42 | connect, 43 | closeConnection, 44 | connectionStatus: getConnectionStatus, 45 | }; 46 | -------------------------------------------------------------------------------- /database/models.js: -------------------------------------------------------------------------------- 1 | const { Schema, model } = require('mongoose'); 2 | const { v4: uuid } = require('uuid'); 3 | 4 | const metadata = { 5 | creationTimestamp: { type: Date, default: new Date() }, 6 | uid: { type: String, default: uuid() }, 7 | name: { type: String, required: true }, 8 | generateName: String, 9 | resourceVersion: { type: String, default: "1" }, 10 | annotations: { 11 | type: Map, 12 | of: String 13 | }, 14 | deletionGracePeriodSeconds: Number, 15 | deletionTimestamp: Number, 16 | finalizers: [ String ], 17 | generateName: String, 18 | generation: Number, 19 | namespace: String, 20 | managedFields: { 21 | apiVersion: String, 22 | fieldsType: String, 23 | fieldsV1: String, 24 | manager: String, 25 | operation: String, 26 | subresource: String, 27 | time: String, 28 | }, 29 | ownerReferences: { 30 | apiVersion: String, 31 | blockOwnerDeletion: Boolean, 32 | controller: Boolean, 33 | kind: String, 34 | name: String, 35 | uid: String, 36 | }, 37 | selfLink: String, 38 | labels: { 39 | type: Map, 40 | of: String 41 | }, 42 | annotations: { 43 | type: Map, 44 | of: String 45 | }, 46 | }; 47 | 48 | const labelSelector = { 49 | matchExpressions: [{ 50 | key: { type: String }, 51 | operator: { type: String }, 52 | values: { type: [ String ], default: undefined }, 53 | }], 54 | matchLabels: { 55 | type: Map, 56 | of: String, 57 | }, 58 | } 59 | 60 | const objectFieldSelector = { 61 | fieldPath: String, 62 | apiVersion: String, 63 | } 64 | 65 | const resourceFieldSelector = { 66 | resource: String, 67 | containerName: String, 68 | divisor: String, 69 | } 70 | 71 | const lifecycleHandler = { 72 | exec: { 73 | command: { type: [ String ], default: undefined }, 74 | httpGet : { 75 | port: Schema.Types.Mixed, 76 | host: String, 77 | httpHeaders: { 78 | name: String, 79 | value: String, 80 | }, 81 | path: String, 82 | scheme: String, 83 | }, 84 | tcpSocket: { 85 | port: Schema.Types.Mixed, 86 | host: String, 87 | } 88 | } 89 | } 90 | 91 | const probe = { 92 | ...lifecycleHandler, 93 | initialDelaySeconds: Number, 94 | terminationGracePeriodSeconds: Number, 95 | periodSeconds: Number, 96 | timeoutSeconds: Number, 97 | failureThreshol: Number, 98 | dsuccessThreshold: Number, 99 | grpc: { 100 | port: Schema.Types.Mixed, 101 | service: String, 102 | } 103 | } 104 | 105 | const container = { 106 | name: String, 107 | image: String, 108 | ports: [{ 109 | containerPort: Number, 110 | protocol: String, 111 | hostIP: String, 112 | hostPort: Number, 113 | name: String, 114 | }], 115 | env: [{ 116 | name: String, 117 | value: String, 118 | valueFrom: { 119 | configMapKeyRef: { 120 | key: String, 121 | name: String, 122 | optional: Boolean, 123 | }, 124 | fieldRef: objectFieldSelector, 125 | resourceFieldRef: resourceFieldSelector, 126 | secretKeyRef: { 127 | key: String, 128 | name: String, 129 | optional: Boolean, 130 | } 131 | } 132 | }], 133 | envFrom: [{ 134 | configMapRef: { 135 | name: String, 136 | optional: Boolean, 137 | }, 138 | prefix: String, 139 | secretRef: { 140 | name: String, 141 | optional: Boolean, 142 | } 143 | }], 144 | volumeMounts: [{ 145 | mountPath: String, 146 | name: String, 147 | mountPropagation: String, 148 | readOnly: Boolean, 149 | subPath: String, 150 | subPathExpr: String, 151 | }], 152 | volumeDevices: [{ 153 | devicePath: String, 154 | name: String, 155 | }], 156 | resources: { 157 | claims: [{ 158 | name: String, 159 | }], 160 | limits: { 161 | type: Map, 162 | of: String 163 | }, 164 | requests: { 165 | type: Map, 166 | of: String 167 | }, 168 | }, 169 | resizePolicy: [{ 170 | resourceName: String, 171 | restartPolicy: String, 172 | }], 173 | lifecycle: { 174 | postStart: lifecycleHandler, 175 | preStop: lifecycleHandler, 176 | }, 177 | terminationMessagePath: String, 178 | terminationMessagePolicy: String, 179 | livenessProbe: probe, 180 | readinessProbe: probe, 181 | startupProbe: probe, 182 | restartPolicy: String, 183 | securityContext: { 184 | runAsUser: Number, 185 | runAsNonRoot: Boolean, 186 | runAsGroup: Number, 187 | readOnlyRootFilesystem: Boolean, 188 | procMount: String, 189 | privileged: Boolean, 190 | allowPrivilegeEscalation: Boolean, 191 | capabilities: { 192 | add: { type: [ String ], default: undefined }, 193 | drop: { type: [ String ], default: undefined }, 194 | }, 195 | seccompProfile: { 196 | type: String, 197 | localhostProfile: String, 198 | }, 199 | seLinuxOptions: { 200 | level: String, 201 | role: String, 202 | type: String, 203 | user: String, 204 | }, 205 | windowsOptions: { 206 | gmsaCredentialSpec: String, 207 | hostProcess: Boolean, 208 | runAsUserName: String, 209 | } 210 | }, 211 | stdin: Boolean, 212 | stdinOnce: Boolean, 213 | tty: Boolean, 214 | command: String, 215 | args: String, 216 | workingDir: String, 217 | terminationMessagePath: String, 218 | terminationMessagePolicy: String, 219 | imagePullPolicy: String, 220 | }; 221 | 222 | const ephemeralContainer = { 223 | name: String, 224 | targetContainerName: String, 225 | } 226 | 227 | const pod = { 228 | apiVersion: String, 229 | kind: String, 230 | metadata, 231 | spec: { 232 | initContainers: { type: [container], default: undefined }, 233 | containers: { type: [container], default: undefined }, 234 | ephemeralContainers: { type: [ephemeralContainer], default: undefined }, 235 | imagePullSecrets: { 236 | name: String, 237 | }, 238 | enableServiceLinks: Boolean, 239 | os: { 240 | name: String, 241 | }, 242 | restartPolicy: String, 243 | activeDeadlineSeconds: Number, 244 | readinessGates: { 245 | conditionType: String, 246 | }, 247 | terminationGracePeriodSeconds: Number, 248 | dnsPolicy: String, 249 | securityContext: {}, 250 | schedulerName: String, 251 | }, 252 | status: { 253 | nominatedNodeName: String, 254 | phase: { 255 | type: String, 256 | enum: [ 'Pending', 'Running', 'Succeeded', 'Failed', 'Unknown' ], 257 | default: 'Pending' 258 | }, 259 | conditions: [{ 260 | type: { type: String }, 261 | status: { 262 | type: String, 263 | enum: [ 'True', 'False', 'Unknown' ], 264 | default: 'False' 265 | }, 266 | lastProbeTime: Date, 267 | reason: String, 268 | message: String, 269 | lastTransitionTime: Date 270 | }], 271 | hostIP: String, 272 | podIP: { type: String, default: null }, 273 | podIPs: [{ 274 | ip: String 275 | }], 276 | startTime: Date, 277 | containerStatuses: [{ 278 | restartCount: { type: Number, default: 0 }, 279 | started: Boolean, 280 | ready: Boolean, 281 | name: String, 282 | state: { 283 | running: { 284 | startedAt: String 285 | } 286 | }, 287 | imageID: String, 288 | image: String, 289 | lastState: {}, 290 | containerID: String 291 | }], 292 | qosClass: String 293 | } 294 | } 295 | 296 | // const podTemplate = { 297 | // metadata, 298 | // spec: pod.spec, 299 | // } 300 | 301 | const statusConditions = { 302 | conditions: { 303 | status: String, 304 | type: String, 305 | lastTransitionTime: Date, 306 | message: String, 307 | reason: String, 308 | } 309 | }; 310 | 311 | const namespaceSchema = Schema({ 312 | apiVersion: String, 313 | kind: String, 314 | metadata, 315 | spec: { 316 | finalizers: [ String ], 317 | }, 318 | status: statusConditions, 319 | }); 320 | 321 | const deploymentSchema = Schema({ 322 | apiVersion: String, 323 | kind: String, 324 | metadata, 325 | spec: { 326 | selector: labelSelector, 327 | template: pod, 328 | replicas: { type: Number, default: 1 }, 329 | minReadySeconds: { type: Number, default: 30 }, 330 | strategy: { 331 | type: { type: String, default: "RollingUpdate"}, 332 | rollingUpdate: { 333 | maxSurge: { type: Schema.Types.Mixed, default: "25%" }, 334 | maxUnavailable: { type: Schema.Types.Mixed, default: "25%" }, 335 | } 336 | }, 337 | revisionHistoryLimit: Number, 338 | progressDeadlineSeconds: Number, 339 | paused: { type: Boolean, default: false }, 340 | }, 341 | status: { 342 | observedGeneration: { type: Number, default: 0 }, 343 | replicas: { type: Number, default: 0 }, 344 | availableReplicas: { type: Number, default: 0 }, 345 | readyReplicas: { type: Number, default: 0 }, 346 | unavailableReplicas: { type: Number, default: 0 }, 347 | updatedReplicas: { type: Number, default: 0 }, 348 | collisionCount: Number, 349 | observedGeneration: Number, 350 | ...statusConditions, 351 | }, 352 | }); 353 | 354 | const podSchema = Schema(pod); 355 | 356 | const ingressLoadBalancerStatus = { 357 | ingress: [{ 358 | hostname: String, 359 | ip: String, 360 | ports: [{ 361 | error: String, 362 | port: Number, 363 | protocol: String, 364 | }], 365 | }] 366 | }; 367 | 368 | const serviceSchema = Schema({ 369 | apiVersion: String, 370 | kind: String, 371 | metadata, 372 | spec: { 373 | allocateLoadBalancerNodePorts: Boolean, 374 | clusterIP: String, 375 | clusterIPs: { type: [ String ], default: [ ] }, 376 | externalIPs: { type: [ String ], default: [ ] }, 377 | externalName: String, 378 | externalTrafficPolicy: String, 379 | healthCheckNodePort: String, 380 | internalTrafficPolicy: { type: String, default: "Cluster" }, 381 | ipFamilies: { type: [ String ], default: [ "IPv4" ] }, 382 | ipFamilyPolicy: String, 383 | loadBalancerClass: String, 384 | loadBalancerIP: String, 385 | loadBalancerSourceRanges: [ String ], 386 | ports: [{ 387 | appProtocol: String, 388 | name: String, 389 | nodePort: Number, 390 | port: { type: Number, required: true }, 391 | protocol: String, 392 | targetPort: Schema.Types.Mixed, 393 | }], 394 | publishNotReadyAddresses: Boolean, 395 | selector: { 396 | type: Map, 397 | of: String 398 | }, 399 | sessionAffinity: { type: String, default: "None" }, 400 | sessionAffinityConfig: { 401 | clientIP: { 402 | timeoutSeconds: Number, 403 | } 404 | }, 405 | type: { type: String }, 406 | }, 407 | status: { 408 | ...statusConditions, 409 | loadBalancer: ingressLoadBalancerStatus, 410 | }, 411 | }); 412 | 413 | const typedLocalObjectReference = { 414 | apiGroup: String, 415 | kind: String, 416 | name: String, 417 | } 418 | 419 | const ingressServiceBackend = { 420 | name: String, 421 | port: { 422 | name: String, 423 | number: Number, 424 | } 425 | }; 426 | 427 | const ingressSchema = Schema({ 428 | apiVersion: String, 429 | kind: String, 430 | metadata, 431 | spec: { 432 | defaultBackend: { 433 | resource: typedLocalObjectReference, 434 | service: ingressServiceBackend, 435 | }, 436 | ingressClassName: String, 437 | rules: { 438 | host: String, 439 | http: { 440 | paths: [{ 441 | path: String, 442 | pathType: { type: String, required: true }, 443 | backend: { 444 | resource: typedLocalObjectReference, 445 | service: ingressServiceBackend, 446 | }, 447 | }] 448 | } 449 | }, 450 | tls: { 451 | hosts: [String], 452 | secretName: String, 453 | } 454 | }, 455 | status: { 456 | loadBalancer: ingressLoadBalancerStatus, 457 | }, 458 | }); 459 | 460 | const dns = Schema({ 461 | name: { 462 | type: String, 463 | required: true 464 | }, 465 | type: { 466 | type: String, 467 | enum: [ 468 | 'A', 469 | 'NS', 470 | 'MD', 471 | 'MF', 472 | 'CNAME', 473 | 'SOA', 474 | 'MB', 475 | 'MG', 476 | 'MR', 477 | 'NULL', 478 | 'WKS', 479 | 'PTR', 480 | 'HINFO', 481 | 'MINFO', 482 | 'MX', 483 | 'TXT', 484 | 'AAAA', 485 | 'SRV', 486 | 'EDNS', 487 | 'SPF', 488 | 'AXFR', 489 | 'MAILB', 490 | 'MAILA', 491 | 'ANY', 492 | 'CAA', 493 | ], 494 | required: true 495 | }, 496 | class: { 497 | type: String, 498 | enum: [ 499 | 'IN', 500 | 'CS', 501 | 'CH', 502 | 'HS', 503 | 'ANY', 504 | ], 505 | required: true 506 | }, 507 | ttl: { 508 | type: Number, 509 | required: true 510 | }, 511 | address: { 512 | type: String, 513 | required: true 514 | }, 515 | }); 516 | 517 | const secretSchema = Schema({ 518 | apiVersion: String, 519 | kind: String, 520 | metadata, 521 | data: { 522 | type: Map, 523 | of: String 524 | }, 525 | immutable: Boolean, 526 | stringData: { 527 | type: Map, 528 | of: String 529 | }, 530 | type: { 531 | type: String, 532 | default: "Opaque", 533 | enum: [ 534 | 'Opaque', 535 | 'kubernetes.io/service-account-token', 536 | 'kubernetes.io/dockercfg', 537 | 'kubernetes.io/dockerconfigjson', 538 | 'kubernetes.io/basic-auth', 539 | 'kubernetes.io/ssh-auth', 540 | 'kubernetes.io/tls', 541 | 'bootstrap.kubernetes.io/token', 542 | ] 543 | }, 544 | }) 545 | 546 | const configMapSchema = Schema({ 547 | apiVersion: String, 548 | kind: String, 549 | metadata, 550 | data: { 551 | type: Map, 552 | of: String 553 | }, 554 | immutable: Boolean, 555 | binaryData: { 556 | type: Map, 557 | of: model.Binary 558 | } 559 | }) 560 | 561 | const endpointsSchema = Schema({ 562 | apiVersion: String, 563 | kind: String, 564 | metadata, 565 | subsets: [{ 566 | addresses: [{ 567 | ip: String, 568 | nodeName: String, 569 | targetRef: { 570 | kind: String, 571 | namespace: String, 572 | name: String, 573 | uid: String 574 | } 575 | }], 576 | notReadyAddresses: [{ 577 | ip: String, 578 | nodeName: String, 579 | targetRef: { 580 | kind: String, 581 | namespace: String, 582 | name: String, 583 | uid: String 584 | } 585 | }], 586 | ports: [{ 587 | name: String, 588 | port: Number, 589 | protocol: String 590 | }] 591 | }] 592 | }) 593 | 594 | const role = { 595 | apiVersion: String, 596 | kind: String, 597 | metadata, 598 | rules: [{ 599 | apiGroups: [String], 600 | resources: [String], 601 | verbs: [String], 602 | resourceNames: [String], 603 | nonResourceURLs: [String], 604 | }] 605 | }; 606 | 607 | const roleSchema = Schema(role); 608 | 609 | const clusterRoleSchema = Schema({ 610 | ...role, 611 | aggregationRule: { 612 | clusterRoleSelectors: [{ 613 | matchExpressions: { 614 | key: String, 615 | operator: String, 616 | values: String, 617 | }, 618 | matchLabels: { 619 | type: Map, 620 | of: String 621 | }, 622 | }] 623 | } 624 | }) 625 | 626 | const roleBindingSchema = Schema({ 627 | apiVersion: String, 628 | kind: String, 629 | metadata, 630 | roleRef: { 631 | apiGroup: String, 632 | kind: String, 633 | name: String, 634 | }, 635 | subjects: [{ 636 | apiGroups: [String], 637 | kind: [String], 638 | namespace: [String], 639 | name: [String], 640 | }] 641 | }) 642 | 643 | const certificateSigningRequestSchema = Schema({ 644 | apiVersion: String, 645 | kind: String, 646 | metadata, 647 | spec: { 648 | expirationSeconds: Number, 649 | extra: { 650 | type: Map, 651 | of: String 652 | }, 653 | groups: [String], 654 | request: String, 655 | signerName: String, 656 | uid: String, 657 | usages: [String], 658 | username: String, 659 | }, 660 | status: { 661 | certificate: String, 662 | ...statusConditions, 663 | } 664 | }) 665 | 666 | module.exports = { 667 | Namespace: model('Namespace', namespaceSchema), 668 | Deployment: model('Deployment', deploymentSchema), 669 | Pod: model('Pod', podSchema), 670 | Service: model('Service', serviceSchema), 671 | Secret: model('Secret', secretSchema), 672 | ConfigMap: model('ConfigMap', configMapSchema), 673 | Ingress: model('Ingress', ingressSchema), 674 | ClusterRole: model('ClusterRole', clusterRoleSchema), 675 | ClusterRoleBinding: model('ClusterRoleBinding', roleBindingSchema), 676 | RoleBinding: model('RoleBinding', roleBindingSchema), 677 | Role: model('Role', roleSchema), 678 | CertificateSigningRequest: model('CertificateSigningRequest', certificateSigningRequestSchema), 679 | Endpoints: model('Endpoints', endpointsSchema), 680 | DNS: model('DNS', dns), 681 | }; 682 | -------------------------------------------------------------------------------- /dns/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /dns/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | 3 | COPY dns/package.json ./package.json 4 | 5 | RUN npm i 6 | 7 | COPY dns/index.js ./index.js 8 | 9 | COPY dns/db.js ./db.js 10 | 11 | CMD ["node", "index.js"] 12 | -------------------------------------------------------------------------------- /dns/db.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { Schema, model, connect } = require('mongoose'); 3 | 4 | connect(process.env.DB_URL); 5 | 6 | const dns = Schema({ 7 | name: { 8 | type: String, 9 | required: true 10 | }, 11 | type: { 12 | type: String, 13 | enum: [ 14 | 'A', 15 | 'NS', 16 | 'MD', 17 | 'MF', 18 | 'CNAME', 19 | 'SOA', 20 | 'MB', 21 | 'MG', 22 | 'MR', 23 | 'NULL', 24 | 'WKS', 25 | 'PTR', 26 | 'HINFO', 27 | 'MINFO', 28 | 'MX', 29 | 'TXT', 30 | 'AAAA', 31 | 'SRV', 32 | 'EDNS', 33 | 'SPF', 34 | 'AXFR', 35 | 'MAILB', 36 | 'MAILA', 37 | 'ANY', 38 | 'CAA', 39 | ], 40 | required: true 41 | }, 42 | class: { 43 | type: String, 44 | enum: [ 45 | 'IN', 46 | 'CS', 47 | 'CH', 48 | 'HS', 49 | 'ANY', 50 | ], 51 | required: true 52 | }, 53 | ttl: { 54 | type: Number, 55 | required: true 56 | }, 57 | address: { 58 | type: String, 59 | required: true 60 | }, 61 | }); 62 | 63 | module.exports = { 64 | DNS: model('DNS', dns), 65 | }; 66 | -------------------------------------------------------------------------------- /dns/index.js: -------------------------------------------------------------------------------- 1 | const dns = require('dns2'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const { DNS } = require('./db.js'); 5 | 6 | let cache = []; 7 | 8 | const { Packet } = dns; 9 | 10 | const server = dns.createServer({ 11 | udp : true, 12 | tcp : true, 13 | // doh : { 14 | // ssl : true, 15 | // cert : fs.readFileSync(path.join(__dirname, 'server.crt')), 16 | // key : fs.readFileSync(path.join(__dirname, 'secret.key')), 17 | // }, 18 | }); 19 | 20 | server.on('request', (request, send, client) => { 21 | const response = Packet.createResponseFromRequest(request); 22 | const [ question ] = request.questions; 23 | const { name } = question; 24 | let record = cache.find((e) => e.record.name === name)?.record; 25 | new Promise(function(resolve, reject) { 26 | if (!record) { 27 | return DNS.findOne({ name }) 28 | .then((res) => { 29 | record = res; 30 | resolve({ cached: false }); 31 | }); 32 | } 33 | resolve({ cached: true }); 34 | }) 35 | .then((data) => { 36 | if (record) { 37 | response.answers.push({ 38 | ...record, 39 | type: Packet.TYPE[record.type], 40 | class: Packet.CLASS[record.class], 41 | }); 42 | send(response); 43 | if (!data.cached) { 44 | cache.push({ 45 | time: Date.now(), 46 | record 47 | }); 48 | } 49 | } 50 | cache.filter((e) => e.time <= (Date.now() - 3600000)); // one hour 51 | }) 52 | }); 53 | 54 | (async() => { 55 | const closed = new Promise(resolve => process.on('SIGINT', resolve)); 56 | await server.listen({ 57 | // doh : 8443, 58 | udp : 5333, 59 | tcp : 5334, 60 | }); 61 | console.log('Listening.'); 62 | console.log(server.addresses()); 63 | await closed; 64 | process.stdout.write('\n'); 65 | await server.close(); 66 | console.log('Closed.'); 67 | process.exit(0); 68 | })(); 69 | -------------------------------------------------------------------------------- /dns/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loadbalancer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./index.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "dns2": "^2.1.0", 13 | "mongoose": "^8.3.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/hello-world/configMap.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "ConfigMap", 4 | "metadata": { 5 | "name": "hello-world" 6 | }, 7 | "data": { 8 | "string-value": "Hello, world!", 9 | "number-value": "42", 10 | "boolean-value": "true", 11 | "multiline-value": "This is a multiline value\nthat spans multiple lines\n", 12 | "list-value": "- item1\n- item2\n- item3\n", 13 | "object-value": "key1: value1\nkey2: value2\n", 14 | "json-value": "{\n \"key\": \"value\",\n \"array\": [1, 2, 3],\n \"nested\": {\n \"innerKey\": \"innerValue\"\n }\n}", 15 | "yaml-value": "key: value\narray:\n - 1\n - 2\n - 3\nnested:\n innerKey: innerValue" 16 | }, 17 | "binaryData": { 18 | "binary-value": "MDEwMTAxMDAwMTEwMTAwMDAxMTAxMDAxMDExMTAwMTEwMDEwMDAwMDAxMTAxMDAxMDExMTAwMTEwMDEwMDAwMDAxMTAwMDAxMDAxMDAwMDAwMTExMDEwMDAxMTAwMTAxMDExMTAwMTEwMTExMDEwMA==" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/hello-world/deployment.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "Deployment", 3 | "apiVersion": "apps/v1", 4 | "metadata": { 5 | "name": "hello-world", 6 | "labels": { 7 | "app": "hello-world", 8 | "namespace": "test" 9 | } 10 | }, 11 | "spec": { 12 | "selector": { 13 | "matchLabels": { 14 | "app": "hello-world" 15 | } 16 | }, 17 | "replicas": 3, 18 | "template": { 19 | "metadata": { 20 | "name": "hello-world", 21 | "labels": { 22 | "app": "hello-world" 23 | } 24 | }, 25 | "spec": { 26 | "containers": [ 27 | { 28 | "name": "hello-world", 29 | "image": "crccheck/hello-world:latest", 30 | "ports": [ 31 | { 32 | "name": "hello-world", 33 | "containerPort": 8000 34 | } 35 | ], 36 | "env": [ 37 | { 38 | "name": "TEST", 39 | "value": "temp" 40 | } 41 | ], 42 | "envFrom": [ 43 | { 44 | "secretRef": { 45 | "name": "hello-world" 46 | } 47 | } 48 | ] 49 | } 50 | ] 51 | } 52 | }, 53 | "strategy": { 54 | "type": "RollingUpdate", 55 | "rollingUpdate": { 56 | "maxUnavailable": "25%", 57 | "maxSurge": "25%" 58 | } 59 | }, 60 | "revisionHistoryLimit": 10, 61 | "progressDeadlineSeconds": 600 62 | }, 63 | "status": {} 64 | } 65 | -------------------------------------------------------------------------------- /examples/hello-world/ingress.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "networking.k8s.io/v1", 3 | "kind": "Ingress", 4 | "metadata": { 5 | "name": "hello-world", 6 | "namespace": "test" 7 | }, 8 | "spec": { 9 | "rules": [ 10 | { 11 | "host": "thisrouteshouldnotexsist.com", 12 | "http": { 13 | "paths": [ 14 | { 15 | "backend": { 16 | "serviceName": "hello-world", 17 | "servicePort": 80 18 | } 19 | } 20 | ] 21 | } 22 | } 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/hello-world/namespace.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "Namespace", 4 | "metadata": { 5 | "name": "default", 6 | "labels": { 7 | "name": "default" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/hello-world/pod.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "Pod", 3 | "apiVersion": "v1", 4 | "metadata": { 5 | "name": "hello-world", 6 | "labels": { 7 | "name": "hello-world", 8 | "role": "hello-world" 9 | } 10 | }, 11 | "spec": { 12 | "containers": [ 13 | { 14 | "name": "hello-world", 15 | "image": "crccheck/hello-world:latest", 16 | "ports": [ 17 | { 18 | "name": "hello-world", 19 | "containerPort": 8000 20 | } 21 | ], 22 | "env": [ 23 | { 24 | "name": "TEST", 25 | "value": "temp" 26 | }, 27 | { 28 | "name": "STRING_VAL", 29 | "valueFrom": { 30 | "configMapKeyRef": { 31 | "name": "hello-world", 32 | "key": "string-value" 33 | } 34 | } 35 | }, 36 | { 37 | "name": "NUMBER_VAL", 38 | "valueFrom": { 39 | "configMapKeyRef": { 40 | "name": "hello-world", 41 | "key": "number-value" 42 | } 43 | } 44 | } 45 | ], 46 | "envFrom": [ 47 | { 48 | "secretRef": { 49 | "name": "hello-world" 50 | } 51 | } 52 | ] 53 | } 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/hello-world/secret.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "Secret", 4 | "metadata": { 5 | "name": "hello-world" 6 | }, 7 | "data": { 8 | "string-value": "SGVsbG8sIHdvcmxkIQ==", 9 | "number-value": "NDI=", 10 | "boolean-value": "dHJ1ZQ==", 11 | "multiline-value": "VGhpcyBpcyBhIG1pbmxpbmUgdmFsdWUgZXZlcnkgdGhhdCBzcGFuIG11bHRpcGxlIGxpbmVzCg==\n", 12 | "json-value": "eyAiY29tcGxleCI6ICIxMjM0NSIsICJzdHJpbmciOiAiSGVsbG8sIHdvcmxkISIsICJuZXN0ZWQiOiB7ICJpbnN0YW5jZSI6IDEsICJzdHJpbmciOiAxLCAibmVzdGVkIjogeyAiaW5uZXJLZXkiOiAiSW5uZXJWYWx1ZSJ9fX0=", 13 | "binary-value": "YmFzZTY0IHN0cmluZwo=" 14 | }, 15 | "stringData": { 16 | "list-value": "item1\nitem2\nitem3\n", 17 | "object-value": "key1: value1\nkey2: value2\nkey3: value3\n", 18 | "yaml-value": "key: value\nstring: Hello, world!\nnested:\n innerKey: innerValue\n" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/hello-world/service.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "Service", 3 | "apiVersion": "v1", 4 | "metadata": { 5 | "name": "hello-world", 6 | "labels": { 7 | "app": "hello-world", 8 | "namespace": "test" 9 | } 10 | }, 11 | "spec": { 12 | "ports": [ 13 | { 14 | "name": "80-tcp", 15 | "protocol": "TCP", 16 | "port": 80, 17 | "targetPort": 8000 18 | } 19 | ], 20 | "selector": { 21 | "app": "hello-world", 22 | "deploymentconfig": "hello-world" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /functions.js: -------------------------------------------------------------------------------- 1 | const { dockerCommand } = require('docker-cli-js'); 2 | const { randomBytes } = require("crypto"); 3 | const portfinder = require('portfinder'); 4 | const { isText, isBinary } = require('istextorbinary'); 5 | 6 | let duration = (timeDiff, loop = true) => { 7 | let y = 365 * 24 * 60 * 60 * 1000; 8 | let d = 24 * 60 * 60 * 10000; 9 | let h = 60 * 60 * 1000; 10 | let m = 60 * 1000; 11 | let s = 1000; 12 | if (timeDiff >= y) { 13 | let val = Math.floor(timeDiff / y); 14 | return `${val}y${duration(timeDiff - (y * val))}`; 15 | } 16 | if (timeDiff >= d) { 17 | let val = Math.floor(timeDiff / d); 18 | return `${val}d${duration(timeDiff - (d * val))}`; 19 | } 20 | if (timeDiff >= h) { 21 | let val = Math.floor(timeDiff / h); 22 | return `${val}h${duration(timeDiff - (h * val))}`; 23 | } 24 | if (timeDiff >= m) { 25 | let val = Math.floor(timeDiff / m); 26 | return `${val}m${duration(timeDiff - (m * val))}`; 27 | } 28 | if (timeDiff >= s) { 29 | return `${Math.floor(timeDiff / s)}s`; 30 | } 31 | return '0s'; 32 | }; 33 | 34 | let imageExists = (imageName, options) => dockerCommand(`inspect --type=image ${imageName.includes(':') ? imageName.split(':')[0] : imageName}`, { echo: false, ...options }) 35 | .then((res) => !(res.length === 0 || (imageName.includes(':') && !res.object.find((e) => e.DockerVersion !== imageName.split(':')[1])))); 36 | let buildImage = (imageName, dockerfile = 'Dockerfile', options) => { 37 | return dockerCommand(`build -t ${imageName} -f ${dockerfile} .`, { ...options }) 38 | }; 39 | let pullImage = (imageName) => dockerCommand(`pull ${imageName}`, { echo: false }); 40 | let dockerExec = (containerName, command) => dockerCommand(`exec -t ${containerName} ${command}`, { echo: false }); 41 | let runImage = async (imageName, containerName, options) => { 42 | let cmd = `run `; 43 | 44 | if (Array.isArray(options?.ports) && options?.ports.length > 0) { 45 | cmd += (await Promise.all(options.ports.map(async (e) => { 46 | return `-p ${e} `; 47 | }))).join(''); 48 | } 49 | if (Array.isArray(options?.expose) && options?.expose.length > 0) { 50 | cmd += (await Promise.all(options.expose.map(async (e) => { 51 | return `--expose ${e} `; 52 | }))).join(''); 53 | } 54 | if (Array.isArray(options?.env) && options?.env.length > 0) { 55 | cmd += options.env.map((e) => `-e ${e.name}='${e.value}' `).join(''); 56 | } 57 | cmd += `--name ${containerName} -itd ${imageName}`; 58 | return dockerCommand(cmd, { echo: false }); 59 | }; 60 | 61 | const isContainerRunning = (containerName) => dockerCommand(`inspect -f '{{.State.Running}}' "${containerName}"`, { echo: false }); 62 | 63 | const stopContainer = (containerName) => dockerCommand(`stop ${containerName}`, { echo: false }); 64 | 65 | const killContainer = (containerName) => dockerCommand(`kill ${containerName}`, { echo: false }); 66 | 67 | const removeContainer = (containerName) => dockerCommand(`rm ${containerName}`, { echo: false }); 68 | 69 | const getContainerIP = (containerName) => dockerCommand(`inspect ${containerName}`, { echo: false }) 70 | 71 | const getAllContainersWithName = (containerName, imageName) => dockerCommand(`ps -q -f name=${containerName} -f ancestor=${imageName}`, { echo: false }); 72 | 73 | // TODO: figure out why `bin/bash` commands don't work 74 | const addPodsToService = (containerName, pods) => { 75 | let ips = pods.map((e) => e.status.podIP); 76 | return dockerExec(containerName, `bin/bash -c '${ips.map((e) => `echo "pod add ${e}" > /proc/1/fd/0`).join(' ; ')}'`) 77 | .then(() => ips); 78 | } 79 | 80 | const addPortsToService = (containerName, ports) => { 81 | return dockerExec(containerName, `bin/bash -c '${ports.map((e) => `echo "port add ${e}" > /proc/1/fd/0`).join(' ; ')}'`) 82 | .then(() => ports); 83 | } 84 | 85 | const addPortToService = (containerName, port) => { 86 | return dockerExec(containerName, `bin/bash -c 'echo "port add ${port}" > /proc/1/fd/0'`); 87 | } 88 | 89 | const addPodToEndpoints = (containerName, podIP) => { 90 | return dockerExec(containerName, `bin/bash -c 'echo "pod add ${podIP}" > /proc/1/fd/0'`); 91 | } 92 | 93 | const removePortFromService = (containerName, port) => { 94 | return dockerExec(containerName, `bin/bash -c 'echo "port remove ${port}" > /proc/1/fd/0'`); 95 | } 96 | 97 | const removePodFromEndpoints = (containerName, podIP) => { 98 | return dockerExec(containerName, `bin/bash -c 'echo "pod remove ${podIP}" > /proc/1/fd/0'`); 99 | } 100 | 101 | module.exports = { 102 | isText, 103 | isContainerRunning, 104 | isBinary, 105 | imageExists, 106 | duration, 107 | getAllContainersWithName, 108 | randomBytes, 109 | addPortsToService, 110 | addPortToService, 111 | addPodToEndpoints, 112 | removePortFromService, 113 | removePodFromEndpoints, 114 | getContainerIP, 115 | buildImage, 116 | pullImage, 117 | runImage, 118 | stopContainer, 119 | killContainer, 120 | removeContainer, 121 | }; 122 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const db = require('./database/connection.js'); 4 | const { api, namespace, namespaceCheck, deployment, endpoints, pod, service, ingress, secret, configMap, certificatesigningrequest, clusterRole, role, clusterRoleBinding, roleBinding, openapi } = require('./routes/index.js'); 5 | const { killContainer, removeContainer } = require('./functions.js'); 6 | const { Object, Status } = require('./objects'); 7 | const nodeCleanup = require('node-cleanup'); 8 | 9 | let dnsServerIndex = process.argv.indexOf('-dnsServer'); 10 | 11 | if (dnsServerIndex === -1) { 12 | console.error('Could not find local DNS server!'); 13 | console.error('Did you run npm start?'); 14 | process.exit(1); 15 | } 16 | 17 | let dbNameIndex = process.argv.indexOf('-dbName'); 18 | 19 | if (dbNameIndex !== -1) { 20 | process.env.DB_URL = `mongodb://localhost:27017`; 21 | } 22 | 23 | process.env.DNS_SERVER = process.argv[dnsServerIndex + 1]; 24 | 25 | db.connect(process.env.DB_URL); 26 | 27 | const app = express(); 28 | 29 | app.use(express.json()); 30 | 31 | app.use((req, res, next) => { 32 | console.log(req.headers, req.body, req.url); 33 | console.log('------------------'); 34 | next(); 35 | }) 36 | 37 | app.use(api); 38 | app.use(openapi); 39 | 40 | app.use(namespace); 41 | app.use(namespaceCheck); 42 | app.use(pod); 43 | app.use(service); 44 | app.use(ingress); 45 | app.use(endpoints); 46 | app.use(deployment); 47 | app.use(secret); 48 | app.use(configMap); 49 | 50 | app.use((req, res) => { 51 | res.status(404).send(Object.notFoundStatus()); 52 | }) 53 | 54 | app.use((err, req, res, next) => { 55 | if (err instanceof Status) { 56 | res.status(err.code).send(err); 57 | } else { 58 | console.error(err.stack); 59 | console.log(err); 60 | res.status(500).send(Object.internalServerErrorStatus()); 61 | } 62 | return next(); 63 | }); 64 | 65 | app.listen(8080); 66 | 67 | nodeCleanup(async (exitCode, signal) => { 68 | if (signal) { 69 | if (dbNameIndex != -1) { 70 | console.log(process.argv[dbNameIndex + 1]); 71 | await killContainer(process.argv[dbNameIndex + 1]); 72 | await removeContainer(process.argv[dbNameIndex + 1]); 73 | } 74 | process.kill(process.pid, signal); 75 | } 76 | }); 77 | -------------------------------------------------------------------------------- /loadBalancer/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /loadBalancer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | 3 | COPY loadBalancer/package.json ./package.json 4 | 5 | ENV PORTS '' 6 | ENV PODS '' 7 | ENV DNS_SERVER '' 8 | 9 | RUN npm i 10 | 11 | COPY loadBalancer/index.js ./index.js 12 | 13 | CMD ["node", "index.js"] 14 | -------------------------------------------------------------------------------- /loadBalancer/index.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const express = require('express'); 3 | const readline = require("readline"); 4 | const dns = require('dns'); 5 | const https = require('https'); 6 | const http = require('http'); 7 | const { pipeline } = require("stream/promises"); 8 | const CacheableLookup = require('cacheable-lookup'); 9 | const cacheable = new CacheableLookup(); 10 | 11 | let pods = []; 12 | let cur = 0; 13 | let ports = {}; 14 | let listeners = []; 15 | let running = true; 16 | 17 | let instance; 18 | 19 | cacheable.servers = ['1.1.1.1', '8.8.8.8']; 20 | 21 | // TODO: Look into https://iximiuz.com/en/posts/multiple-containers-same-port-reverse-proxy/ 22 | 23 | if ((process.env.DNS_SERVER || '').trim() !== '') { 24 | cacheable.servers.unshift(process.env.DNS_SERVER.trim()); 25 | } 26 | 27 | const handler = (port) => { 28 | return async (req, res) => { 29 | try { 30 | let url = `http://${pods[cur]}:${port}${req.originalUrl}` 31 | console.log(`Forwarding to: ${url}`); 32 | req.params = {}; 33 | const { data } = await axios[req.method.toLowerCase()](url, { 34 | ...req, 35 | lookup: cacheable.lookup 36 | }, { 37 | responseType: 'stream', 38 | }); 39 | await pipeline(data, res); 40 | } catch (e) { 41 | if (e.response) { 42 | return res.status(e.response.status).send(e.response.data); 43 | } 44 | console.error(e); 45 | res.sendStatus(503); 46 | } 47 | cur = (cur + 1) % pods.length; 48 | };; 49 | } 50 | 51 | function removePort(port) { 52 | let p = port.split(':'); 53 | let l = listeners.findIndex((e) => port === e.address().port); 54 | if (p.length === 2 && l !== -1) { 55 | console.log(`Closing port: ${port}`); 56 | ports[p[1]] = undefined; 57 | l[p[1]].close(); 58 | listeners.splice(l, 1); 59 | } 60 | } 61 | 62 | function addPort(port) { 63 | let p = port.split(':'); 64 | let l = listeners.findIndex((e) => port === e.address().port); 65 | if (p.length === 2 && l === -1) { 66 | ports[p[1]] = p[0]; 67 | const app = express(); 68 | app.all('*', handler(Number(p[1]))); 69 | console.log(`Opening server on: ${p[0]} and forwarding to ${p[1]}`); 70 | listeners.push(app.listen(Number(p[0]))); 71 | } 72 | } 73 | 74 | function removePod(podIP) { 75 | let p = pods.findIndex((e) => e === podIP); 76 | if (p) { 77 | console.log(`Removing pod: ${podIP}`); 78 | pods.splice(p, 1); 79 | } 80 | } 81 | 82 | function addPod(podIP) { 83 | if (!pods.includes(podIP)) { 84 | console.log(`Adding pod: ${podIP}`); 85 | pods.push(podIP); 86 | } 87 | } 88 | 89 | let portIndex = process.argv.findIndex((e) => e === '-ports'); 90 | let podIndex = process.argv.findIndex((e) => e === '-pods'); 91 | if (portIndex !== -1 && podIndex !== -1) { 92 | if (process.argv.findIndex((e) => e === '-pods') < process.argv.findIndex((e) => e === '-ports')) { 93 | process.argv.slice(portIndex + 1).forEach(addPort); 94 | process.argv.slice(podIndex + 1, portIndex).forEach(addPod); 95 | } else { 96 | process.argv.slice(portIndex + 1, podIndex).forEach(addPort); 97 | process.argv.slice(podIndex + 1).forEach(addPod); 98 | } 99 | } else if (process.env.PORTS && process.env.PODS) { 100 | process.env.PORTS.split(' ').forEach(addPort); 101 | process.env.PODS.split(' ').forEach(addPod); 102 | } 103 | 104 | const cli = readline.createInterface({ 105 | input: process.stdin, 106 | output: process.stdout, 107 | prompt: "\n", 108 | }); 109 | 110 | cli.prompt(); 111 | 112 | cli.on("line", (line) => { 113 | let cmd = line.split(' '); 114 | if (cmd[0] === 'port') { 115 | if (cmd[1] === 'add') { 116 | addPort(cmd[2]); 117 | } else if (cmd[1] === 'remove') { 118 | removePort(cmd[2]); 119 | } else if (cmd[1] === 'list') { 120 | console.log(ports); 121 | } 122 | } else if (cmd[0] === 'pod') { 123 | if (cmd[1] === 'add') { 124 | addPod(cmd[2]); 125 | } else if (cmd[1] === 'remove') { 126 | removePod(cmd[2]); 127 | } else if (cmd[1] === 'list') { 128 | console.log(pods); 129 | } 130 | } else if (cmd[0] === 'kill') { 131 | process.exit(0); 132 | } 133 | 134 | cli.prompt(); 135 | }); 136 | -------------------------------------------------------------------------------- /loadBalancer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loadbalancer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./index.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "axios": "^1.6.8", 13 | "cacheable-lookup": "^6.1.0", 14 | "express": "^4.19.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /middleware/general.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | findOne(Model) { 3 | return (req, res, next) => { 4 | let query = { 'metadata.name': req.params.name, 'metadata.namespace': req.params.namespace }; 5 | if (!req.params.namespace) { 6 | query = { 'metadata.name': req.params.name }; 7 | } 8 | Model.findOne(query) 9 | .then((item) => { 10 | if (item) { 11 | return res.status(200).send(item); 12 | } 13 | return res.status(404).send(Model.notFoundStatus(req.params.name)); 14 | }) 15 | .catch(next); 16 | }; 17 | }, 18 | list(Model) { 19 | return (req, res, next) => { 20 | if (req.headers?.accept?.split(';').find((e) => e === 'as=Table')) { 21 | return Model.table(req.query) 22 | .then((table) => res.status(200).send(table)) 23 | .catch(next); 24 | } 25 | Model.list(req.query) 26 | .then((list) => res.status(200).send(list)) 27 | .catch(next); 28 | }; 29 | }, 30 | save(Model) { 31 | return (req, res, next) => { 32 | if (!req.body?.metadata?.creationTimestamp) { 33 | req.body.metadata.creationTimestamp = new Date(); 34 | } 35 | if (!req.body?.metadata?.namespace) { 36 | req.body.metadata.namespace = (req.params.namespace || "default"); 37 | } 38 | Model.create(req.body) 39 | .then((item) => res.status(201).send(item)) 40 | .catch(next); 41 | }; 42 | }, 43 | update(Model) { 44 | return (req, res, next) => { 45 | let query = { 'metadata.name': req.params.name, 'metadata.namespace': req.params.namespace }; 46 | if (!req.params.namespace) { 47 | query = { 'metadata.name': req.params.name }; 48 | } 49 | if (Object.keys(req.body).length > 0) { 50 | Model.findOne(query) 51 | .then((item) => item ? item.update(req.body) : Promise.resolve()) 52 | .then((item) => { 53 | if (item) { 54 | return res.status(201).send(item); 55 | } 56 | return res.status(404).send(Model.notFoundStatus(req.params.name)); 57 | }) 58 | .catch(next); 59 | } else { 60 | Model.findOne(query) 61 | .then((item) => { 62 | if (item) { 63 | return res.status(200).send(item); 64 | } 65 | return res.status(404).send(Model.notFoundStatus(req.params.name)); 66 | }) 67 | .catch(next); 68 | } 69 | } 70 | }, 71 | patch(Model) { 72 | return (req, res, next) => { 73 | let query = { 'metadata.name': req.params.name, 'metadata.namespace': req.params.namespace }; 74 | if (!req.params.namespace) { 75 | query = { 'metadata.name': req.params.name }; 76 | } 77 | if (Object.keys(req.body).length > 0) { 78 | Model.findOne(query) 79 | .then((item) => item ? item.update(req.body) : Promise.resolve()) 80 | .then((item) => { 81 | if (item) { 82 | return res.status(201).send(item); 83 | } 84 | return res.status(404).send(Model.notFoundStatus(req.params.name)); 85 | }) 86 | .catch(next); 87 | } else { 88 | Model.findOne(query) 89 | .then((item) => item ? res.status(200).send(item) : res.status(200).send({})) 90 | .catch(next); 91 | } 92 | }; 93 | }, 94 | deleteOne(Model) { 95 | return (req, res, next) => { 96 | let query = { 'metadata.name': req.params.name, 'metadata.namespace': req.params.namespace }; 97 | if (!req.params.namespace) { 98 | query = { 'metadata.name': req.params.name }; 99 | } 100 | Model.findOne(query) 101 | .then((item) => item ? item.delete() : Promise.resolve()) 102 | .then((item) => { 103 | if (item) { 104 | return res.status(200).send(item.successfulStatus()); 105 | } 106 | return res.status(404).send(Model.notFoundStatus(req.params.name)); 107 | }) 108 | .catch(next); 109 | }; 110 | }, 111 | delete(Model) { 112 | return (req, res, next) => { 113 | Model.find(req.body) 114 | .then((items) => Promise.all(items.map((item) => item.delete()))) 115 | .then((item) => items ? res.status(200).send(item) : res.status(200).send({})) 116 | .catch(next); 117 | }; 118 | } 119 | }; 120 | -------------------------------------------------------------------------------- /middleware/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | general: require('./general.js'), 3 | openapi: require('./openapi.js'), 4 | }; 5 | -------------------------------------------------------------------------------- /middleware/openapi.js: -------------------------------------------------------------------------------- 1 | const OpenApi = require('@wesleytodd/openapi'); 2 | const OpenApiV3 = require('@wesleytodd/openapi'); 3 | const ApiV1OpenApiV3 = require('@wesleytodd/openapi'); 4 | const ApiNetworkingK8sIoV1OpenApiV3 = require('@wesleytodd/openapi'); 5 | const ApiAppsV1OpenApiV3 = require('@wesleytodd/openapi'); 6 | const ApiRbacAuthorizatonK8sIoV1OpenApiV3 = require('@wesleytodd/openapi'); 7 | const ApiCertificatesK8sIoApiV3 = require('@wesleytodd/openapi'); 8 | const openApiV3Spec = require('../openApiSpecs/v3/api.json'); 9 | const apiV1OpenApiV3Spec = require('../openApiSpecs/v3/api/v1.json'); 10 | const apiNetworkingK8sIoV1OpenApiV3Spec = require('../openApiSpecs/v3/apis/networking.k8s.io/v1.json'); 11 | const apiAppsV1OpenApiV3Spec = require('../openApiSpecs/v3/apis/apps/v1.json'); 12 | const apiRbacAuthorizatonK8sIoV1OpenApiV3Spec = require('../openApiSpecs/v3/apis/rbac.authorization.k8s.io/v1.json'); 13 | const apiCertificatesK8sIoApiV3Spec = require('../openApiSpecs/v3/apis/certificates.k8s.io/v1.json'); 14 | 15 | const openapi = new OpenApi({ 16 | openapi: "3.0.0", 17 | }); 18 | 19 | module.exports = { 20 | openapiv3: OpenApiV3(openApiV3Spec), 21 | apiV1OpenapiV3: ApiV1OpenApiV3(apiV1OpenApiV3Spec), 22 | apiNetworkingK8sIoV1OpenApiV3: ApiNetworkingK8sIoV1OpenApiV3(apiNetworkingK8sIoV1OpenApiV3Spec), 23 | apiAppsV1OpenApiV3: ApiAppsV1OpenApiV3(apiAppsV1OpenApiV3Spec), 24 | apiRbacAuthorizatonK8sIoV1OpenApiV3: ApiNetworkingK8sIoV1OpenApiV3(apiRbacAuthorizatonK8sIoV1OpenApiV3Spec), 25 | apiCertificatesK8sIoApiV3: ApiAppsV1OpenApiV3(apiCertificatesK8sIoApiV3Spec), 26 | validSchema: (schema) => { 27 | return (req, res, next) => { 28 | let path = Object.keys(schema.document.paths) 29 | .filter((e) => { 30 | let r = new RegExp(e.replace(/\{.*?\}/ig, '.*?')); 31 | if (req.baseUrl.match(r)?.length === 1) { 32 | return Object.keys(schema.document.paths); 33 | } 34 | }) 35 | .reverse() 36 | .at(0); 37 | return openapi.validPath(schema.document.paths[path])(req, res, next); 38 | }; 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /objects/certificateSigningRequest.js: -------------------------------------------------------------------------------- 1 | const K8Object = require('./object.js'); 2 | const { CertificateSigningRequest: Model } = require('../database/models.js'); 3 | const { duration } = require('../functions.js'); 4 | 5 | class CertificateSigningRequest extends K8Object { 6 | constructor(config) { 7 | super(config); 8 | this.rules = config.rules; 9 | } 10 | 11 | static apiVersion = 'v1'; 12 | static kind = 'CertificateSigningRequest'; 13 | 14 | 15 | static findOne(params = {}, options = {}) { 16 | return Model.findOne(params, options) 17 | .then((certificateSigningRequest) => { 18 | if (certificateSigningRequest) { 19 | return new CertificateSigningRequest(certificateSigningRequest).setResourceVersion(); 20 | } 21 | }); 22 | } 23 | 24 | static find(params = {}, projection = {}, queryOptions = {}) { 25 | let options = { 26 | sort: { 'metadata.name': 1 }, 27 | ...queryOptions 28 | }; 29 | return Model.find(params, projection, options) 30 | .then((certificateSigningRequests) => { 31 | if (certificateSigningRequests) { 32 | return Promise.all(certificateSigningRequests.map((certificateSigningRequest) => new CertificateSigningRequest(certificateSigningRequest).setResourceVersion())); 33 | } 34 | }); 35 | } 36 | 37 | static create(config) { 38 | return this.findOne({ 'metadata.name': this.metadata.name }) 39 | .then((existingRole) => { 40 | if (existingRole) { 41 | throw this.alreadyExistsStatus(config.metadata.name); 42 | } 43 | return new Model(config).save(); 44 | }) 45 | .then((certificateSigningRequest) => new CertificateSigningRequest(certificateSigningRequest)); 46 | } 47 | 48 | delete () { 49 | return Model.findOneAndDelete({ 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }) 50 | .then((certificateSigningRequest) => { 51 | if (certificateSigningRequest) { 52 | return this.setConfig(certificateSigningRequest); 53 | } 54 | }); 55 | } 56 | 57 | static findAllSorted(queryOptions = {}, sortOptions = { 'created_at': 1 }) { 58 | let params = { 59 | 'metadata.certificateSigningRequest': queryOptions.certificateSigningRequest ? queryOptions.certificateSigningRequest : undefined, 60 | 'metadata.resourceVersion': queryOptions.resourceVersionMatch ? queryOptions.resourceVersionMatch : undefined, 61 | }; 62 | if (!([...new Set(Object.values(params))].find((e) => undefined))) { 63 | params = {}; 64 | } 65 | let projection = {}; 66 | let options = { 67 | sort: sortOptions, 68 | limit: queryOptions.limit ? Number(queryOptions.limit) : undefined, 69 | }; 70 | return this.find(params, projection, options); 71 | } 72 | 73 | static list (queryOptions = {}) { 74 | return this.findAllSorted(queryOptions) 75 | .then(async (certificateSigningRequests) => ({ 76 | apiVersion: this.apiVersion, 77 | kind: `${this.kind}List`, 78 | metadata: { 79 | continue: false, 80 | remainingItemCount: queryOptions.limit && queryOptions.limit < certificateSigningRequests.length ? certificateSigningRequests.length - queryOptions.limit : 0, 81 | resourceVersion: `${await super.hash(`${certificateSigningRequests.length}${JSON.stringify(certificateSigningRequests[0])}`)}` 82 | }, 83 | items: certificateSigningRequests 84 | })); 85 | } 86 | 87 | static table (queryOptions = {}) { 88 | return this.findAllSorted(queryOptions) 89 | .then(async (certificateSigningRequests) => ({ 90 | "kind": "Table", 91 | "apiVersion": "meta.k8s.io/v1", 92 | "metadata": { 93 | "resourceVersion": `${await super.hash(`${certificateSigningRequests.length}${JSON.stringify(certificateSigningRequests[0])}`)}`, 94 | }, 95 | "columnDefinitions": [ 96 | { 97 | "name": "Name", 98 | "type": "string", 99 | "format": "name", 100 | "description": "Name must be unique within a certificateSigningRequest. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names", 101 | "priority": 0 102 | }, 103 | { 104 | "name": "Age", 105 | "type": "string", 106 | "format": "", 107 | "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", 108 | "priority": 0 109 | }, 110 | ], 111 | "rows": certificateSigningRequests.map((e) => ({ 112 | "cells": [ 113 | e.metadata.name, 114 | duration(new Date() - e.metadata.creationTimestamp), 115 | ], 116 | object: { 117 | "kind": "PartialObjectMetadata", 118 | "apiVersion": "meta.k8s.io/v1", 119 | metadata: e.metadata, 120 | } 121 | })), 122 | })); 123 | } 124 | 125 | static notFoundStatus(objectName = undefined) { 126 | return super.notFoundStatus(this.kind, objectName); 127 | } 128 | 129 | static forbiddenStatus(objectName = undefined) { 130 | return super.forbiddenStatus(this.kind, objectName); 131 | } 132 | 133 | static alreadyExistsStatus(objectName = undefined) { 134 | return super.alreadyExistsStatus(this.kind, objectName); 135 | } 136 | 137 | static unprocessableContentStatus(objectName = undefined, message = undefined) { 138 | return super.unprocessableContentStatus(this.kind, objectName, undefined, message); 139 | } 140 | 141 | update(updateObj, options = {}) { 142 | return Model.findOneAndUpdate( 143 | { 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }, 144 | updateObj, 145 | { 146 | new: true, 147 | ...options, 148 | } 149 | ) 150 | .then((certificateSigningRequest) => { 151 | if (certificateSigningRequest) { 152 | return this.setConfig(certificateSigningRequest); 153 | } 154 | }); 155 | } 156 | 157 | async setConfig(config) { 158 | await super.setResourceVersion(); 159 | this.data = config.data; 160 | return this; 161 | } 162 | } 163 | 164 | module.exports = CertificateSigningRequest; 165 | -------------------------------------------------------------------------------- /objects/clusterRole.js: -------------------------------------------------------------------------------- 1 | const K8Object = require('./object.js'); 2 | const { ClusterRole: Model } = require('../database/models.js'); 3 | const { duration } = require('../functions.js'); 4 | 5 | class ClusterRole extends K8Object { 6 | constructor(config) { 7 | super(config); 8 | this.rules = config.rules; 9 | } 10 | 11 | static apiVersion = 'v1'; 12 | static kind = 'ClusterRole'; 13 | 14 | 15 | static findOne(params = {}, options = {}) { 16 | return Model.findOne(params, options) 17 | .then((clusterRole) => { 18 | if (clusterRole) { 19 | return new ClusterRole(clusterRole).setResourceVersion(); 20 | } 21 | }); 22 | } 23 | 24 | static find(params = {}, projection = {}, queryOptions = {}) { 25 | let options = { 26 | sort: { 'metadata.name': 1 }, 27 | ...queryOptions 28 | }; 29 | return Model.find(params, projection, options) 30 | .then((clusterRoles) => { 31 | if (clusterRoles) { 32 | return Promise.all(clusterRoles.map((clusterRole) => new ClusterRole(clusterRole).setResourceVersion())); 33 | } 34 | }); 35 | } 36 | 37 | static create(config) { 38 | return this.findOne({ 'metadata.name': config.metadata.name, 'metadata.namespace': config.metadata.namespace }) 39 | .then((existingRole) => { 40 | if (existingRole) { 41 | throw this.alreadyExistsStatus(config.metadata.name); 42 | } 43 | return new Model(config).save(); 44 | }) 45 | .then((clusterRole) => new ClusterRole(clusterRole)); 46 | } 47 | 48 | delete () { 49 | return Model.findOneAndDelete({ 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }) 50 | .then((clusterRole) => { 51 | if (clusterRole) { 52 | return this.setConfig(clusterRole); 53 | } 54 | }); 55 | } 56 | 57 | static findAllSorted(queryOptions = {}, sortOptions = { 'created_at': 1 }) { 58 | let params = { 59 | 'metadata.clusterRole': queryOptions.clusterRole ? queryOptions.clusterRole : undefined, 60 | 'metadata.resourceVersion': queryOptions.resourceVersionMatch ? queryOptions.resourceVersionMatch : undefined, 61 | }; 62 | if (!([...new Set(Object.values(params))].find((e) => undefined))) { 63 | params = {}; 64 | } 65 | let projection = {}; 66 | let options = { 67 | sort: sortOptions, 68 | limit: queryOptions.limit ? Number(queryOptions.limit) : undefined, 69 | }; 70 | return this.find(params, projection, options); 71 | } 72 | 73 | static list (queryOptions = {}) { 74 | return this.findAllSorted(queryOptions) 75 | .then(async (clusterRoles) => ({ 76 | apiVersion: this.apiVersion, 77 | kind: `${this.kind}List`, 78 | metadata: { 79 | continue: false, 80 | remainingItemCount: queryOptions.limit && queryOptions.limit < clusterRoles.length ? clusterRoles.length - queryOptions.limit : 0, 81 | resourceVersion: `${await super.hash(`${clusterRoles.length}${JSON.stringify(clusterRoles[0])}`)}` 82 | }, 83 | items: clusterRoles 84 | })); 85 | } 86 | 87 | static table (queryOptions = {}) { 88 | return this.findAllSorted(queryOptions) 89 | .then(async (clusterRoles) => ({ 90 | "kind": "Table", 91 | "apiVersion": "meta.k8s.io/v1", 92 | "metadata": { 93 | "resourceVersion": `${await super.hash(`${clusterRoles.length}${JSON.stringify(clusterRoles[0])}`)}`, 94 | }, 95 | "columnDefinitions": [ 96 | { 97 | "name": "Name", 98 | "type": "string", 99 | "format": "name", 100 | "description": "Name must be unique within a clusterRole. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names", 101 | "priority": 0 102 | }, 103 | { 104 | "name": "Age", 105 | "type": "string", 106 | "format": "", 107 | "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", 108 | "priority": 0 109 | }, 110 | ], 111 | "rows": clusterRoles.map((e) => ({ 112 | "cells": [ 113 | e.metadata.name, 114 | duration(new Date() - e.metadata.creationTimestamp), 115 | ], 116 | object: { 117 | "kind": "PartialObjectMetadata", 118 | "apiVersion": "meta.k8s.io/v1", 119 | metadata: e.metadata, 120 | } 121 | })), 122 | })); 123 | } 124 | 125 | static notFoundStatus(objectName = undefined) { 126 | return super.notFoundStatus(this.kind, objectName); 127 | } 128 | 129 | static forbiddenStatus(objectName = undefined) { 130 | return super.forbiddenStatus(this.kind, objectName); 131 | } 132 | 133 | static alreadyExistsStatus(objectName = undefined) { 134 | return super.alreadyExistsStatus(this.kind, objectName); 135 | } 136 | 137 | static unprocessableContentStatus(objectName = undefined, message = undefined) { 138 | return super.unprocessableContentStatus(this.kind, objectName, undefined, message); 139 | } 140 | 141 | update(updateObj, options = {}) { 142 | return Model.findOneAndUpdate( 143 | { 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }, 144 | updateObj, 145 | { 146 | new: true, 147 | ...options, 148 | } 149 | ) 150 | .then((clusterRole) => { 151 | if (clusterRole) { 152 | return this.setConfig(clusterRole); 153 | } 154 | }); 155 | } 156 | 157 | async setConfig(config) { 158 | await super.setResourceVersion(); 159 | this.data = config.data; 160 | return this; 161 | } 162 | } 163 | 164 | module.exports = ClusterRole; 165 | -------------------------------------------------------------------------------- /objects/clusterRoleBinding.js: -------------------------------------------------------------------------------- 1 | const K8Object = require('./object.js'); 2 | const { ClusterRoleBinding: Model } = require('../database/models.js'); 3 | const { duration } = require('../functions.js'); 4 | 5 | class ClusterRoleBinding extends K8Object { 6 | constructor(config) { 7 | super(config); 8 | this.rules = config.rules; 9 | } 10 | 11 | static apiVersion = 'v1'; 12 | static kind = 'ClusterRoleBinding'; 13 | 14 | 15 | static findOne(params = {}, options = {}) { 16 | return Model.findOne(params, options) 17 | .then((clusterRoleBinding) => { 18 | if (clusterRoleBinding) { 19 | return new ClusterRoleBinding(clusterRoleBinding).setResourceVersion(); 20 | } 21 | }); 22 | } 23 | 24 | static find(params = {}, projection = {}, queryOptions = {}) { 25 | let options = { 26 | sort: { 'metadata.name': 1 }, 27 | ...queryOptions 28 | }; 29 | return Model.find(params, projection, options) 30 | .then((clusterRoleBindings) => { 31 | if (clusterRoleBindings) { 32 | return Promise.all(clusterRoleBindings.map((clusterRoleBinding) => new ClusterRoleBinding(clusterRoleBinding).setResourceVersion())); 33 | } 34 | }); 35 | } 36 | 37 | static create(config) { 38 | return this.findOne({ 'metadata.name': config.metadata.name, 'metadata.namespace': config.metadata.namespace }) 39 | .then((existingRole) => { 40 | if (existingRole) { 41 | throw this.alreadyExistsStatus(config.metadata.name); 42 | } 43 | return new Model(config).save(); 44 | }) 45 | .then((clusterRoleBinding) => new ClusterRoleBinding(clusterRoleBinding)); 46 | } 47 | 48 | delete () { 49 | return Model.findOneAndDelete({ 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }) 50 | .then((clusterRoleBinding) => { 51 | if (clusterRoleBinding) { 52 | return this.setConfig(clusterRoleBinding); 53 | } 54 | }); 55 | } 56 | 57 | static findAllSorted(queryOptions = {}, sortOptions = { 'created_at': 1 }) { 58 | let params = { 59 | 'metadata.clusterRoleBinding': queryOptions.clusterRoleBinding ? queryOptions.clusterRoleBinding : undefined, 60 | 'metadata.resourceVersion': queryOptions.resourceVersionMatch ? queryOptions.resourceVersionMatch : undefined, 61 | }; 62 | if (!([...new Set(Object.values(params))].find((e) => undefined))) { 63 | params = {}; 64 | } 65 | let projection = {}; 66 | let options = { 67 | sort: sortOptions, 68 | limit: queryOptions.limit ? Number(queryOptions.limit) : undefined, 69 | }; 70 | return this.find(params, projection, options); 71 | } 72 | 73 | static list (queryOptions = {}) { 74 | return this.findAllSorted(queryOptions) 75 | .then(async (clusterRoleBindings) => ({ 76 | apiVersion: this.apiVersion, 77 | kind: `${this.kind}List`, 78 | metadata: { 79 | continue: false, 80 | remainingItemCount: queryOptions.limit && queryOptions.limit < clusterRoleBindings.length ? clusterRoleBindings.length - queryOptions.limit : 0, 81 | resourceVersion: `${await super.hash(`${clusterRoleBindings.length}${JSON.stringify(clusterRoleBindings[0])}`)}` 82 | }, 83 | items: clusterRoleBindings 84 | })); 85 | } 86 | 87 | static table (queryOptions = {}) { 88 | return this.findAllSorted(queryOptions) 89 | .then(async (clusterRoleBindings) => ({ 90 | "kind": "Table", 91 | "apiVersion": "meta.k8s.io/v1", 92 | "metadata": { 93 | "resourceVersion": `${await super.hash(`${clusterRoleBindings.length}${JSON.stringify(clusterRoleBindings[0])}`)}`, 94 | }, 95 | "columnDefinitions": [ 96 | { 97 | "name": "Name", 98 | "type": "string", 99 | "format": "name", 100 | "description": "Name must be unique within a clusterRoleBinding. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names", 101 | "priority": 0 102 | }, 103 | { 104 | "name": "Age", 105 | "type": "string", 106 | "format": "", 107 | "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", 108 | "priority": 0 109 | }, 110 | ], 111 | "rows": clusterRoleBindings.map((e) => ({ 112 | "cells": [ 113 | e.metadata.name, 114 | duration(new Date() - e.metadata.creationTimestamp), 115 | ], 116 | object: { 117 | "kind": "PartialObjectMetadata", 118 | "apiVersion": "meta.k8s.io/v1", 119 | metadata: e.metadata, 120 | } 121 | })), 122 | })); 123 | } 124 | 125 | static notFoundStatus(objectName = undefined) { 126 | return super.notFoundStatus(this.kind, objectName); 127 | } 128 | 129 | static forbiddenStatus(objectName = undefined) { 130 | return super.forbiddenStatus(this.kind, objectName); 131 | } 132 | 133 | static alreadyExistsStatus(objectName = undefined) { 134 | return super.alreadyExistsStatus(this.kind, objectName); 135 | } 136 | 137 | static unprocessableContentStatus(objectName = undefined, message = undefined) { 138 | return super.unprocessableContentStatus(this.kind, objectName, undefined, message); 139 | } 140 | 141 | update(updateObj, options = {}) { 142 | return Model.findOneAndUpdate( 143 | { 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }, 144 | updateObj, 145 | { 146 | new: true, 147 | ...options, 148 | } 149 | ) 150 | .then((clusterRoleBinding) => { 151 | if (clusterRoleBinding) { 152 | return this.setConfig(clusterRoleBinding); 153 | } 154 | }); 155 | } 156 | 157 | async setConfig(config) { 158 | await super.setResourceVersion(); 159 | this.data = config.data; 160 | return this; 161 | } 162 | } 163 | 164 | module.exports = ClusterRoleBinding; 165 | -------------------------------------------------------------------------------- /objects/configMap.js: -------------------------------------------------------------------------------- 1 | const K8Object = require('./object.js'); 2 | const { ConfigMap: Model } = require('../database/models.js'); 3 | const { isText, isBinary, duration } = require('../functions.js'); 4 | 5 | class ConfigMap extends K8Object { 6 | constructor(config) { 7 | super(config); 8 | this.data = config.data; 9 | this.binaryData = config.binaryData; 10 | this.immutable = config.immutable; 11 | } 12 | 13 | static apiVersion = 'v1'; 14 | static kind = 'ConfigMap'; 15 | 16 | static findOne(params = {}, projection = {}, options = {}) { 17 | return Model.findOne(params, projection, options) 18 | .then((configMap) => { 19 | if (configMap) { 20 | return new ConfigMap(configMap).setResourceVersion(); 21 | } 22 | }); 23 | } 24 | 25 | static find(params = {}, projection = {}, options = {}) { 26 | return Model.find(params, projection, options) 27 | .then((configMaps) => { 28 | if (configMaps) { 29 | return Promise.all(configMaps.map((configMap) => new ConfigMap(configMap).setResourceVersion())); 30 | } 31 | }); 32 | } 33 | 34 | static create(config) { 35 | const base64RegExp = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/; 36 | const isBase64 = (str) => base64RegExp.test(str); 37 | 38 | return this.findOne({ 'metadata.name': config.metadata.name, 'metadata.namespace': config.metadata.namespace }) 39 | .then((existingConfigMap) => { 40 | if (existingConfigMap) { 41 | throw this.alreadyExistsStatus(config.metadata.name); 42 | } 43 | if (config.data) { 44 | Object.entries(config.data).forEach(([key, value]) => { 45 | if (!isText(null, Buffer.from(value))) { 46 | throw this.unprocessableContentStatus(undefined, `Value for ${key} is not UTF-8`); 47 | } 48 | }); 49 | } 50 | if (config.binaryData) { 51 | Object.entries(config.binaryData).forEach(([key, value]) => { 52 | if (!isBinary(null, Buffer.from(Buffer.from(value, 'base64').toString('binary'), 'base64'))) { 53 | throw this.unprocessableContentStatus(undefined, `Value for ${key} is not Binary`); 54 | } 55 | }); 56 | } 57 | return new Model(config).save(); 58 | }) 59 | .then((configMap) => new ConfigMap(configMap)); 60 | } 61 | 62 | static findAllSorted(queryOptions = {}, sortOptions = { 'created_at': 1 }) { 63 | let params = { 64 | 'metadata.namespace': queryOptions.namespace ? queryOptions.namespace : undefined, 65 | 'metadata.resourceVersion': queryOptions.resourceVersionMatch ? queryOptions.resourceVersionMatch : undefined, 66 | }; 67 | if (!([...new Set(Object.values(params))].find((e) => undefined))) { 68 | params = {}; 69 | } 70 | let projection = {}; 71 | let options = { 72 | sort: sortOptions, 73 | limit: queryOptions.limit ? Number(queryOptions.limit) : undefined, 74 | }; 75 | return this.find(params, projection, options); 76 | } 77 | 78 | static list (queryOptions = {}) { 79 | return this.findAllSorted(queryOptions) 80 | .then(async (configMaps) => ({ 81 | apiVersion: this.apiVersion, 82 | kind: `${this.kind}List`, 83 | metadata: { 84 | continue: false, 85 | remainingItemCount: queryOptions.limit && queryOptions.limit < configMaps.length ? configMaps.length - queryOptions.limit : 0, 86 | resourceVersion: `${await super.hash(`${configMaps.length}${JSON.stringify(configMaps[0])}`)}` 87 | }, 88 | items: configMaps 89 | })); 90 | } 91 | 92 | static table (queryOptions = {}) { 93 | return this.findAllSorted(queryOptions) 94 | .then(async (configMaps) => ({ 95 | "kind": "Table", 96 | "apiVersion": "meta.k8s.io/v1", 97 | "metadata": { 98 | "resourceVersion": `${await super.hash(`${configMaps.length}${JSON.stringify(configMaps[0])}`)}`, 99 | }, 100 | "columnDefinitions": [ 101 | { 102 | "name": "Name", 103 | "type": "string", 104 | "format": "name", 105 | "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", 106 | "priority": 0 107 | }, 108 | { 109 | "name": "Data", 110 | "type": "string", 111 | "format": "", 112 | "description": "Data contains the configuration data. Each key must consist of alphanumeric characters, '-', '_' or '.'. Values with non-UTF-8 byte sequences must use the BinaryData field. The keys stored in Data must not overlap with the keys in the BinaryData field, this is enforced during validation process.", 113 | "priority": 0 114 | }, 115 | { 116 | "name": "BinaryData", 117 | "type": "string", 118 | "format": "", 119 | "description": "BinaryData contains the binary data. Each key must consist of alphanumeric characters, '-', '_' or '.'. BinaryData can contain byte sequences that are not in the UTF-8 range. The keys stored in BinaryData must not overlap with the ones in the Data field, this is enforced during validation process. Using this field will require 1.10+ apiserver and kubelet.", 120 | "priority": 1 121 | }, 122 | { 123 | "name": "Immutable", 124 | "type": "boolean", 125 | "format": "", 126 | "description": "Immutable, if set to true, ensures that data stored in the ConfigMap cannot be updated (only object metadata can be modified). If not set to true, the field can be modified at any time. Defaulted to nil.", 127 | "priority": 1 128 | }, 129 | { 130 | "name": "Age", 131 | "type": "string", 132 | "format": "", 133 | "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", 134 | "priority": 0 135 | } 136 | ], 137 | "rows": configMaps.map((e) => ({ 138 | "cells": [ 139 | e.metadata.name, 140 | Object.keys(e.data).length, 141 | Object.keys(e.binaryData).length, 142 | e.immutable ?? false, 143 | duration(new Date() - e.metadata.creationTimestamp), 144 | ], 145 | object: { 146 | "kind": "PartialObjectMetadata", 147 | "apiVersion": "meta.k8s.io/v1", 148 | metadata: e.metadata, 149 | } 150 | })), 151 | })); 152 | } 153 | 154 | delete () { 155 | return Model.findOneAndDelete({ 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }) 156 | .then((configMap) => { 157 | if (configMap) { 158 | return this.setConfig(configMap); 159 | } 160 | }); 161 | } 162 | 163 | static notFoundStatus(objectName = undefined) { 164 | return super.notFoundStatus(this.kind, objectName); 165 | } 166 | 167 | static forbiddenStatus(objectName = undefined) { 168 | return super.forbiddenStatus(this.kind, objectName); 169 | } 170 | 171 | static alreadyExistsStatus(objectName = undefined) { 172 | return super.alreadyExistsStatus(this.kind, objectName); 173 | } 174 | 175 | static unprocessableContentStatus(objectName = undefined, message = undefined) { 176 | return super.unprocessableContentStatus(this.kind, objectName, undefined, message); 177 | } 178 | 179 | update(updateObj, options = {}) { 180 | if (this?.immutable === true) { 181 | throw new Error(`ConfigMap ${config.metadata.name} is immutable`); 182 | } 183 | return Model.findOneAndUpdate( 184 | { 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }, 185 | updateObj, 186 | { 187 | new: true, 188 | ...options, 189 | } 190 | ) 191 | .then((configMap) => { 192 | if (configMap) { 193 | return this.setConfig(configMap); 194 | } 195 | }); 196 | } 197 | 198 | async setResourceVersion() { 199 | await super.setResourceVersion(); 200 | return this; 201 | } 202 | 203 | mapVariables() { 204 | return [ 205 | [...this.data] 206 | .map(([key, value]) => ({ 207 | name: key, 208 | value, 209 | })), 210 | [...this.binaryData] 211 | .map(([key, value]) => ({ 212 | name: key, 213 | value: Buffer.from(Buffer.from(value, 'base64').toString('binary'), 'base64').toString(), 214 | })) 215 | ].flat(); 216 | } 217 | 218 | async setConfig(config) { 219 | await super.setResourceVersion(); 220 | this.data = config.data; 221 | return this; 222 | } 223 | } 224 | 225 | module.exports = ConfigMap; 226 | -------------------------------------------------------------------------------- /objects/deployment.js: -------------------------------------------------------------------------------- 1 | const K8Object = require('./object.js'); 2 | const Pod = require('./pod.js'); 3 | const Service = require('./service.js'); 4 | const { Deployment: Model } = require('../database/models.js'); 5 | const { 6 | runImage, 7 | getContainerIP, 8 | getAllContainersWithName, 9 | duration, 10 | } = require('../functions.js'); 11 | 12 | class Deployment extends K8Object { 13 | constructor(config) { 14 | super(config); 15 | this.spec = config.spec; 16 | this.status = config.status; 17 | this.rollingOut = false; 18 | } 19 | 20 | static apiVersion = 'v1'; 21 | static kind = 'Deployment'; 22 | 23 | static findOne(params) { 24 | return Model.findOne(params) 25 | .then((deployment) => { 26 | if (deployment) { 27 | return new Deployment(deployment).setResourceVersion(); 28 | } 29 | }); 30 | } 31 | 32 | static find(params) { 33 | return Model.find(params) 34 | .then((deployments) => { 35 | if (deployments) { 36 | return Promise.all(deployments.map((deployment) => new Deployment(deployment).setResourceVersion())); 37 | } 38 | }); 39 | } 40 | 41 | static create(config) { 42 | return this.findOne({ 'metadata.name': config.metadata.name, 'metadata.namespace': config.metadata.namespace }) 43 | .then((existingDeployment) => { 44 | if (existingDeployment) { 45 | throw this.alreadyExistsStatus(config.metadata.name); 46 | } 47 | return new Model(config).save() 48 | }) 49 | .then((deployment) => { 50 | let newDeployment = new Deployment(deployment); 51 | if (newDeployment.spec.paused !== true) { 52 | newDeployment.rollout(); 53 | } 54 | setInterval(() => { 55 | if (newDeployment.rollingOut === false) { 56 | Promise.all( 57 | newDeployment.spec.template.spec.containers 58 | .map((e) => getAllContainersWithName(newDeployment.spec.template.metadata.name, e.image)) 59 | ) 60 | .then((containers) => containers.map((e) => e.raw)) 61 | .then((raw) => raw.toString().split('\n').filter((e) => e !== '')) 62 | .then((arr) => { 63 | if (newDeployment.rollingOut === false) { 64 | if (newDeployment.spec.replicas > arr.length) { 65 | newDeployment.rollout(newDeployment.spec.replicas - arr.length); 66 | } else if (newDeployment.spec.replicas < arr.length) { 67 | new Array(arr.length - newDeployment.spec.replicas) 68 | .fill(0).forEach(() => newDeployment.deletePod()); 69 | } 70 | } 71 | }) 72 | } 73 | }, 1000); 74 | return newDeployment; 75 | }) 76 | } 77 | 78 | delete () { 79 | return Model.findOneAndDelete({ 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }) 80 | .then((deployment) => { 81 | if (deployment) { 82 | return this.setConfig(deployment); 83 | } 84 | }); 85 | } 86 | 87 | update(updateObj) { 88 | return Model.findOneAndUpdate( 89 | { 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }, 90 | updateObj, 91 | { new: true } 92 | ) 93 | .then((deployment) => { 94 | if (deployment) { 95 | let newDeployment = this.setConfig(deployment); 96 | if (newDeployment.spec.paused !== true && 97 | this.rollingOut === false && 98 | JSON.stringify(this.spec.template) !== JSON.stringify(deployment.spec.template)) { 99 | newDeployment.rollout(); 100 | } 101 | return newDeployment; 102 | } 103 | }); 104 | } 105 | 106 | static findAllSorted(queryOptions = {}, sortOptions = { 'created_at': 1 }) { 107 | let params = { 108 | 'metadata.namespace': queryOptions.namespace ? queryOptions.namespace : undefined, 109 | 'metadata.resourceVersion': queryOptions.resourceVersionMatch ? queryOptions.resourceVersionMatch : undefined, 110 | }; 111 | if (!([...new Set(Object.values(params))].find((e) => undefined))) { 112 | params = {}; 113 | } 114 | let projection = {}; 115 | let options = { 116 | sort: sortOptions, 117 | limit: queryOptions.limit ? Number(queryOptions.limit) : undefined, 118 | }; 119 | return this.find(params, projection, options); 120 | } 121 | 122 | static list (queryOptions = {}) { 123 | return this.findAllSorted(queryOptions) 124 | .then(async (deployments) => ({ 125 | apiVersion: this.apiVersion, 126 | kind: `${this.kind}List`, 127 | metadata: { 128 | continue: false, 129 | remainingItemCount: queryOptions.limit && queryOptions.limit < deployments.length ? deployments.length - queryOptions.limit : 0, 130 | resourceVersion: `${await super.hash(`${deployments.length}${JSON.stringify(deployments[0])}`)}` 131 | }, 132 | items: deployments 133 | })); 134 | } 135 | 136 | static table (queryOptions = {}) { 137 | return this.findAllSorted(queryOptions) 138 | .then(async (deployments) => ({ 139 | "kind": "Table", 140 | "apiVersion": "meta.k8s.io/v1", 141 | "metadata": { 142 | "resourceVersion": `${await super.hash(`${deployments.length}${JSON.stringify(deployments[0])}`)}`, 143 | }, 144 | "columnDefinitions": [ 145 | { 146 | "name": "Name", 147 | "type": "string", 148 | "format": "name", 149 | "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names", 150 | "priority": 0 151 | }, 152 | { 153 | "name": "Ready", 154 | "type": "string", 155 | "format": "", 156 | "description": "Number of the pod with ready state", 157 | "priority": 0 158 | }, 159 | { 160 | "name": "Up-to-date", 161 | "type": "string", 162 | "format": "", 163 | "description": "Total number of non-terminated pods targeted by this deployment that have the desired template spec.", 164 | "priority": 0 165 | }, 166 | { 167 | "name": "Available", 168 | "type": "string", 169 | "format": "", 170 | "description": "Total number of available pods (ready for at least minReadySeconds) targeted by this deployment.", 171 | "priority": 0 172 | }, 173 | { 174 | "name": "Age", 175 | "type": "string", 176 | "format": "", 177 | "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", 178 | "priority": 0 179 | }, 180 | { 181 | "name": "Containers", 182 | "type": "string", 183 | "format": "", 184 | "description": "Names of each container in the template.", 185 | "priority": 1 186 | }, 187 | { 188 | "name": "Images", 189 | "type": "string", 190 | "format": "", 191 | "description": "Images referenced by each container in the template.", 192 | "priority": 1 193 | }, 194 | { 195 | "name": "Selector", 196 | "type": "string", 197 | "format": "", 198 | "description": "Label selector for pods. Existing ReplicaSets whose pods are selected by this will be the ones affected by this deployment.", 199 | "priority": 1 200 | } 201 | ], 202 | "rows": deployments.map((e) => ({ 203 | "cells": [ 204 | e.metadata.name, 205 | `${e.status.availableReplicas}/${e.spec.replicas}`, 206 | e.status.updatedReplicas, 207 | e.status.availableReplicas, 208 | duration(new Date() - e.metadata.creationTimestamp), 209 | e.spec.template.spec.containers.map((e) => e.name).join(', '), 210 | e.spec.template.spec.containers.map((e) => e.image).join(', '), 211 | Object.values(e.spec.selector.matchLabels).join(', '), 212 | ], 213 | object: { 214 | "kind": "PartialObjectMetadata", 215 | "apiVersion": "meta.k8s.io/v1", 216 | metadata: e.metadata, 217 | } 218 | })), 219 | })); 220 | } 221 | 222 | static notFoundStatus(objectName = undefined) { 223 | return super.notFoundStatus(this.kind, objectName); 224 | } 225 | 226 | static forbiddenStatus(objectName = undefined) { 227 | return super.forbiddenStatus(this.kind, objectName); 228 | } 229 | 230 | static alreadyExistsStatus(objectName = undefined) { 231 | return super.alreadyExistsStatus(this.kind, objectName); 232 | } 233 | 234 | static unprocessableContentStatus(objectName = undefined, message = undefined) { 235 | return super.unprocessableContentStatus(this.kind, objectName, undefined, message); 236 | } 237 | 238 | async setConfig(config) { 239 | await super.setResourceVersion(); 240 | this.spec = config.spec; 241 | this.status = config.status; 242 | return this; 243 | } 244 | 245 | getSpec() { 246 | return this.spec; 247 | } 248 | 249 | getStatus() { 250 | return this.status; 251 | } 252 | 253 | deletePod() { 254 | return Pod.find( 255 | { 'metadata.name': this.spec.template.metadata.name }, 256 | {}, 257 | { 258 | sort: { 259 | 'created_at': 1 260 | } 261 | } 262 | ) 263 | .then((pods) => { 264 | if (pods[0]) { 265 | return Promise.all([ 266 | pods[0].stop(), 267 | pods[0].delete(), 268 | Service.findOne({ 269 | 'metadata.name': this.metadata.name 270 | }) 271 | .then((service) => { 272 | if (service) { 273 | service.removePod(pod[0]) 274 | } 275 | }) 276 | ]) 277 | .then(() => { 278 | this.update({ 279 | $inc: { 280 | 'status.readyReplicas': -1, 281 | 'status.replicas': -1, 282 | 'status.availableReplicas': -1, 283 | }, 284 | }) 285 | }) 286 | } 287 | }) 288 | } 289 | 290 | async createPod(config) { 291 | if (!config?.metadata?.labels) { 292 | config.metadata.labels = new Map(); 293 | } 294 | config.metadata.labels.set('app', this.metadata.name); 295 | if (!config?.metadata?.namespace) { 296 | config.metadata.namespace = this.metadata.namespace 297 | } 298 | return Pod.create(config) 299 | .then((newPod) => { 300 | return Promise.all([ 301 | this.update({ 302 | $inc: { 303 | 'status.replicas': 1, 304 | 'status.readyReplicas': 1, 305 | 'status.availableReplicas': 1, 306 | }, 307 | $set: { 308 | conditions: [{ 309 | "type": "Progressing", 310 | "status": "True", 311 | "lastUpdateTime": new Date(), 312 | "lastTransitionTime": new Date(), 313 | }, 314 | { 315 | "type": "Available", 316 | "status": "True", 317 | "lastUpdateTime": new Date(), 318 | "lastTransitionTime": new Date(), 319 | }] 320 | } 321 | }), 322 | Service.findOne({ 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }) 323 | .then((service) => { 324 | if (service) { 325 | return service.addPod(newPod); 326 | } 327 | }) 328 | ]) 329 | }) 330 | .then(() => { 331 | if (this?.status?.replicas > this?.spec?.replicas) { 332 | return this.deletePod(); 333 | } 334 | }) 335 | } 336 | 337 | async setResourceVersion() { 338 | await super.setResourceVersion(); 339 | return this; 340 | } 341 | 342 | async rollout(numPods = this.spec.replicas) { 343 | if (this.rollingOut === false) { 344 | this.rollingOut = true; 345 | if (this.spec.strategy.type === "RollingUpdate") { 346 | let percent = Number(`${this.spec.strategy.rollingUpdate.maxUnavailable}`.match(/\d*/)[0]);; 347 | let newPods = 0; 348 | do { 349 | await Promise.all( 350 | new Array(Math.ceil(numPods * percent / 100)) 351 | .fill(0) 352 | .map(() => { 353 | newPods += 1; 354 | return this.createPod(this.spec.template); 355 | }) 356 | ); 357 | } while (this.status.replicas < numPods); 358 | } else if (this.spec.strategy.type === "Recreate") { 359 | Promise.all( 360 | new Array(numPods) 361 | .fill(0) 362 | .map(() => { 363 | return this.deletePod(); 364 | }) 365 | ).then(() => 366 | new Array(numPods) 367 | .fill(0) 368 | .map(() => { 369 | return this.createPod(this.spec.template); 370 | }) 371 | ) 372 | } 373 | } 374 | this.rollingOut = false; 375 | } 376 | } 377 | 378 | module.exports = Deployment; 379 | -------------------------------------------------------------------------------- /objects/endpoints.js: -------------------------------------------------------------------------------- 1 | const K8Object = require('./object.js'); 2 | const Pod = require('./pod.js'); 3 | const { Endpoints: Model, DNS } = require('../database/models.js'); 4 | const { 5 | addPortsToEndpoints, 6 | addPortToEndpoints, 7 | addPodToEndpoints, 8 | removePortFromEndpoints, 9 | removePodFromEndpoints, 10 | pullImage, 11 | imageExists, 12 | buildImage, 13 | runImage, 14 | getContainerIP, 15 | duration, 16 | } = require('../functions.js'); 17 | 18 | class Endpoints extends K8Object { 19 | constructor(config) { 20 | super(config); 21 | this.subsets = config.subsets; 22 | } 23 | 24 | static apiVersion = 'v1'; 25 | static kind = 'Endpoints'; 26 | 27 | static findOne(params) { 28 | return Model.findOne(params) 29 | .then((endpoints) => { 30 | if (endpoints) { 31 | return new Endpoints(endpoints).setResourceVersion(); 32 | } 33 | }); 34 | } 35 | 36 | static find(params) { 37 | return Model.find(params) 38 | .then((endpointses) => { 39 | if (endpointses) { 40 | return Promise.all(endpointses.map((endpoints) => new Endpoints(endpoints).setResourceVersion())); 41 | } 42 | }); 43 | } 44 | 45 | static create(config) { 46 | return this.findOne({ 'metadata.name': config.metadata.name, 'metadata.namespace': config.metadata.namespace }) 47 | .then((existingEndpoints) => { 48 | if (existingEndpoints) { 49 | throw this.alreadyExistsStatus(config.metadata.name); 50 | } 51 | return new Model(config).save() 52 | }) 53 | .then((endpoints) => { 54 | let newEndpoints = new Endpoints(endpoints); 55 | return imageExists('loadbalancer') 56 | .catch((err) => 57 | pullImage('node') 58 | .then(() => buildImage('loadbalancer', 'loadBalancer/Dockerfile')) 59 | ) 60 | .then(() => { 61 | let options = { 62 | ports: newEndpoints.subsets.map((s) => s.ports.map((p) => `${p.port}:${p.port}`)), 63 | env: [{ 64 | name: 'PORTS', 65 | value: config.ports, 66 | }, { 67 | name: 'DNS_SERVER', 68 | value: process.env.DNS_SERVER, 69 | }, { 70 | name: 'PODS', 71 | value: endpoints.subsets.map((e) => e.notReadyAddresses.map((a) => a.ip)).join(' '), 72 | }] 73 | }; 74 | return runImage('loadbalancer', `${newEndpoints.metadata.name}-loadBalancer`, options) 75 | }) 76 | .then(() => newEndpoints.listenForPods()) 77 | }); 78 | } 79 | 80 | getPods() { 81 | return this.subsets.map((s) => s.ports.map((p) => { 82 | return s.notReadyAddresses.map((a) => `${a}:${p}`); 83 | }).join(',')).join(';'); 84 | } 85 | 86 | podEventHandler(pod) { 87 | let podEvents = pod.events(); 88 | let readyMap = (podRef, i) => { 89 | let obj = {}; 90 | obj[`subsets.${i}.addresses`] = podRef; 91 | return obj; 92 | } 93 | let notReadyMap = (podRef, i) => { 94 | let obj = {}; 95 | obj[`subsets.${i}.notReadyAddresses`] = podRef; 96 | return obj; 97 | } 98 | podEvents.on('NewContainer', (podRef) => { 99 | this.update( 100 | null, 101 | { 102 | $push: this.subsets.map(notReadyMap), 103 | }); 104 | }); 105 | podEvents.on('Ready', (pod) => { 106 | this.update( 107 | null, 108 | { 109 | $push: this.subsets.map((s, i) => readyMap( 110 | s.notReadyAddresses.find((e) => e.ip === pod.status.podIP), 111 | i 112 | )), 113 | $pull: this.subsets.map((s, i) => notReadyMap( 114 | s.notReadyAddresses.find((e) => e.ip === pod.status.podIP), 115 | i 116 | )), 117 | }); 118 | this.addPod(p.status.podIP); 119 | }); 120 | podEvents.on('Delete', (pod) => { 121 | this.update( 122 | null, 123 | { 124 | $pull: this.subsets.map((s, i) => notReadyMap( 125 | s.addresses.find((e) => e.ip === pod.status.podIP), 126 | i 127 | )), 128 | }); 129 | this.removePod(p.status.podIP); 130 | }); 131 | } 132 | 133 | listenForPods() { 134 | return Pod.find({ 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }) 135 | .then((pods) => { 136 | pods.forEach(this.podEventHandler); 137 | return this; 138 | }); 139 | } 140 | 141 | delete() { 142 | return Model.findOneAndDelete({ 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }) 143 | .then((endpoints) => { 144 | if (endpoints) { 145 | return this.setConfig(endpoints); 146 | } 147 | }); 148 | } 149 | 150 | update(findObj = {}, updateObj = {}, options = {}) { 151 | return Model.findOneAndUpdate( 152 | { 153 | 'metadata.name': this.metadata.name, 154 | 'metadata.namespace': config.metadata.namespace, 155 | ...findObj, 156 | }, 157 | updateObj, 158 | { 159 | new: true, 160 | ...options, 161 | } 162 | ) 163 | .then((endpoints) => { 164 | if (endpoints) { 165 | return this.setConfig(endpoints); 166 | } 167 | }); 168 | } 169 | 170 | static notFoundStatus(objectName = undefined) { 171 | return super.notFoundStatus(this.kind, objectName); 172 | } 173 | 174 | static forbiddenStatus(objectName = undefined) { 175 | return super.forbiddenStatus(this.kind, objectName); 176 | } 177 | 178 | static alreadyExistsStatus(objectName = undefined) { 179 | return super.alreadyExistsStatus(this.kind, objectName); 180 | } 181 | 182 | static unprocessableContentStatus(objectName = undefined, message = undefined) { 183 | return super.unprocessableContentStatus(this.kind, objectName, undefined, message); 184 | } 185 | 186 | static findAllSorted(queryOptions = {}, sortOptions = { 'created_at': 1 }) { 187 | let params = { 188 | 'metadata.namespace': queryOptions.namespace ? queryOptions.namespace : undefined, 189 | 'metadata.resourceVersion': queryOptions.resourceVersionMatch ? queryOptions.resourceVersionMatch : undefined, 190 | }; 191 | if (!([...new Set(Object.values(params))].find((e) => undefined))) { 192 | params = {}; 193 | } 194 | let projection = {}; 195 | let options = { 196 | sort: sortOptions, 197 | limit: queryOptions.limit ? Number(queryOptions.limit) : undefined, 198 | }; 199 | return this.find(params, projection, options); 200 | } 201 | 202 | static list (queryOptions = {}) { 203 | return this.findAllSorted(queryOptions) 204 | .then(async (endpointses) => ({ 205 | apiVersion: this.apiVersion, 206 | kind: `${this.kind}List`, 207 | metadata: { 208 | continue: false, 209 | remainingItemCount: queryOptions.limit && queryOptions.limit < endpointses.length ? endpointses.length - queryOptions.limit : 0, 210 | resourceVersion: `${await super.hash(`${endpointses.length}${JSON.stringify(endpointses[0])}`)}` 211 | }, 212 | items: endpointses 213 | })); 214 | } 215 | 216 | static table (queryOptions = {}) { 217 | return this.findAllSorted(queryOptions) 218 | .then(async (endpointses) => ({ 219 | "kind": "Table", 220 | "apiVersion": "meta.k8s.io/v1", 221 | "metadata": { 222 | "resourceVersion": `${await super.hash(`${endpointses.length}${JSON.stringify(endpointses[0])}`)}`, 223 | }, 224 | "columnDefinitions": [ 225 | { 226 | "name": "Name", 227 | "type": "string", 228 | "format": "name", 229 | "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names", 230 | "priority": 0 231 | }, 232 | { 233 | "name": "Subsets", 234 | "type": "string", 235 | "format": "name", 236 | "description": "The set of all endpoints is the union of all subsets. Addresses are placed into subsets according to the IPs they share. A single address with multiple ports, some of which are ready and some of which are not (because they come from different containers) will result in the address being displayed in different subsets for the different ports. No address will appear in both Addresses and NotReadyAddresses in the same subset. Sets of addresses and ports that comprise a service", 237 | "priority": 0 238 | } 239 | ], 240 | "rows": pods.map((e) => ({ 241 | "cells": [ 242 | e.metadata.name, 243 | (this.getPods() || ''), 244 | ], 245 | object: { 246 | "kind": "PartialObjectMetadata", 247 | "apiVersion": "meta.k8s.io/v1", 248 | metadata: e.metadata, 249 | } 250 | })), 251 | })); 252 | } 253 | 254 | async setConfig(config) { 255 | await super.setResourceVersion(); 256 | this.subsets = config.subsets; 257 | return this; 258 | } 259 | 260 | async setResourceVersion() { 261 | await super.setResourceVersion(); 262 | return this; 263 | } 264 | 265 | getSubsets() { 266 | return this.subsets; 267 | } 268 | 269 | removePod(pod) { 270 | return removePodFromEndpoints(`${this.metadata.name}-loadBalancer`, pod.status.podIP); 271 | } 272 | 273 | addPod(pod) { 274 | return addPodToEndpoints(`${this.metadata.name}-loadBalancer`, pod.status.podIP); 275 | } 276 | } 277 | 278 | module.exports = Endpoints; 279 | -------------------------------------------------------------------------------- /objects/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Deployment: require('./deployment.js'), 3 | Namespace: require('./namespace.js'), 4 | Object: require('./object.js'), 5 | Service: require('./service.js'), 6 | Status: require('./status.js'), 7 | Secret: require('./secret.js'), 8 | Ingress: require('./ingress.js'), 9 | ConfigMap: require('./configMap.js'), 10 | Endpoints: require('./endpoints.js'), 11 | CertificateSigningRequest: require('./certificateSigningRequest.js'), 12 | ClusterRole: require('./clusterRole.js'), 13 | Role: require('./role.js'), 14 | ClusterRoleBinding: require('./clusterRoleBinding.js'), 15 | RoleBinding: require('./roleBinding.js'), 16 | Pod: require('./pod.js'), 17 | }; 18 | -------------------------------------------------------------------------------- /objects/ingress.js: -------------------------------------------------------------------------------- 1 | const K8Object = require('./object.js'); 2 | const Pod = require('./pod.js'); 3 | const Service = require('./service.js'); 4 | const { Ingress: Model, DNS } = require('../database/models.js'); 5 | const { duration } = require('../functions.js'); 6 | 7 | class Ingress extends K8Object { 8 | constructor(config) { 9 | super(config); 10 | this.spec = config.spec; 11 | this.status = config.status; 12 | } 13 | 14 | static apiGroup = 'networking.k8s.io'; 15 | static apiVersion = `${this.apiGroup}/v1`; 16 | static kind = 'Ingress'; 17 | 18 | static findOne(params) { 19 | return Model.findOne(params) 20 | .then((ingress) => { 21 | if (ingress) { 22 | return new Ingress(ingress).setResourceVersion(); 23 | } 24 | }); 25 | } 26 | 27 | static find(params) { 28 | return Model.find(params) 29 | .then((ingresses) => { 30 | if (ingresses) { 31 | return Promise.all(ingresses.map((ingress) => new Ingress(ingress).setResourceVersion())); 32 | } 33 | }); 34 | } 35 | 36 | static create(config) { 37 | return this.findOne({ 'metadata.name': config.metadata.name, 'metadata.namespace': config.metadata.namespace }) 38 | .then((existingIngress) => { 39 | if (existingIngress) { 40 | throw new Error(this.alreadyExistsStatus(config.metadata.name, this.apiGroup)); 41 | } 42 | return new Model(config).save() 43 | }) 44 | .then(async (ingress) => { 45 | await Promise.all( 46 | req.body.spec.rules.map((rule) => { 47 | return rule.http.paths.map((path) => { 48 | return Service.findOne({ 'metadata.name': path.backend.serviceName }) 49 | .then((service) => { 50 | let arr = [] 51 | if (service?.externalIPs?.length > 0) { 52 | arr.push( 53 | ...service.externalIPs.map((e) => { 54 | new DNS({ 55 | name: rule.host, 56 | type: 'A', 57 | class: 'IN', 58 | ttl: 300, 59 | address: e, 60 | }).save() 61 | }) 62 | ); 63 | } 64 | }) 65 | }) 66 | .flat() 67 | .filter((e) => e); 68 | })); 69 | return ingress; 70 | }) 71 | } 72 | 73 | delete () { 74 | return Model.findOneAndDelete({ 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }) 75 | .then((ingress) => { 76 | if (ingress) { 77 | return this.setConfig(ingress); 78 | } 79 | }); 80 | } 81 | 82 | update(updateObj) { 83 | return Model.findOneAndUpdate( 84 | { 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }, 85 | updateObj, 86 | { new: true } 87 | ) 88 | .then((ingress) => { 89 | if (ingress) { 90 | return this.setConfig(ingress); 91 | } 92 | }); 93 | } 94 | 95 | static findAllSorted(queryOptions = {}, sortOptions = { 'created_at': 1 }) { 96 | let params = { 97 | 'metadata.namespace': queryOptions.namespace ? queryOptions.namespace : undefined, 98 | 'metadata.resourceVersion': queryOptions.resourceVersionMatch ? queryOptions.resourceVersionMatch : undefined, 99 | }; 100 | if (!([...new Set(Object.values(params))].find((e) => undefined))) { 101 | params = {}; 102 | } 103 | let projection = {}; 104 | let options = { 105 | sort: sortOptions, 106 | limit: queryOptions.limit ? Number(queryOptions.limit) : undefined, 107 | }; 108 | return this.find(params, projection, options); 109 | } 110 | 111 | static list (queryOptions = {}) { 112 | return this.findAllSorted(queryOptions) 113 | .then(async (ingresses) => ({ 114 | apiVersion: this.apiVersion, 115 | kind: `${this.kind}List`, 116 | metadata: { 117 | continue: false, 118 | remainingItemCount: queryOptions.limit && queryOptions.limit < ingresses.length ? ingresses.length - queryOptions.limit : 0, 119 | resourceVersion: `${await super.hash(`${ingresses.length}${JSON.stringify(ingresses[0])}`)}` 120 | }, 121 | items: ingresses 122 | })); 123 | } 124 | 125 | static table (queryOptions = {}) { 126 | return this.findAllSorted(queryOptions) 127 | .then(async (ingresses) => ({ 128 | "kind": "Table", 129 | "apiVersion": "meta.k8s.io/v1", 130 | "metadata": { 131 | "resourceVersion": `${await super.hash(`${ingresses.length}${JSON.stringify(ingresses[0])}`)}`, 132 | }, 133 | "columnDefinitions": [ 134 | { 135 | "name": "Name", 136 | "type": "string", 137 | "format": "name", 138 | "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names", 139 | "priority": 0 140 | }, 141 | { 142 | "name": "Type", 143 | "type": "string", 144 | "format": "name", 145 | "description": "Type of service.", 146 | "priority": 0 147 | }, 148 | { 149 | "name": "Cluster-ip", 150 | "type": "string", 151 | "format": "", 152 | "description": "IP within the cluster.", 153 | "priority": 0 154 | }, 155 | { 156 | "name": "External-ip", 157 | "type": "string", 158 | "format": "", 159 | "description": "IP outside the cluster.", 160 | "priority": 0 161 | }, 162 | { 163 | "name": "Port(s)", 164 | "type": "string", 165 | "format": "", 166 | "description": "Port(s) exposed by the service, for the pod(s).", 167 | "priority": 0 168 | }, 169 | { 170 | "name": "Age", 171 | "type": "string", 172 | "format": "", 173 | "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", 174 | "priority": 0 175 | }, 176 | { 177 | "name": "Selector", 178 | "type": "string", 179 | "format": "", 180 | "description": "Which pod(s) are fronted by the service.", 181 | "priority": 1 182 | } 183 | ], 184 | "rows": pods.map((e) => ({ 185 | "cells": [ 186 | e.metadata.name, 187 | e.spec.type, 188 | (e.spec.clusterIP || e.spec.clusterIPs?.join() || ''), 189 | (e.spec.externalIPs?.join() || ''), 190 | e.spec?.ports?.length > 0 ? e.spec.ports.map((e) => `${e.port}/${e.protocol}`).join() : '', 191 | duration(new Date() - e.metadata.creationTimestamp), 192 | e.spec?.selector && Object.keys(e.spec.selector).length > 0 ? Object.entries(e.spec.selector).map((e) => `${e[0]}=${e[1]}`).join() : '', 193 | ], 194 | object: { 195 | "kind": "PartialObjectMetadata", 196 | "apiVersion": "meta.k8s.io/v1", 197 | metadata: e.metadata, 198 | } 199 | })), 200 | })); 201 | } 202 | 203 | static notFoundStatus(objectName = undefined) { 204 | return super.notFoundStatus(this.kind, objectName, this.apiGroup); 205 | } 206 | 207 | static forbiddenStatus(objectName = undefined) { 208 | return super.forbiddenStatus(this.kind, objectName, this.apiGroup); 209 | } 210 | 211 | static alreadyExistsStatus(objectName = undefined) { 212 | return super.alreadyExistsStatus(this.kind, objectName); 213 | } 214 | 215 | static unprocessableContentStatus(objectName = undefined, message = undefined) { 216 | return super.unprocessableContentStatus(this.kind, objectName, this.apiGroup, message); 217 | } 218 | 219 | async setConfig(config) { 220 | await super.setResourceVersion(); 221 | this.spec = config.spec; 222 | this.status = config.status; 223 | return this; 224 | } 225 | 226 | async setResourceVersion() { 227 | await super.setResourceVersion(); 228 | return this; 229 | } 230 | 231 | getSpec() { 232 | return this.spec; 233 | } 234 | 235 | getStatus() { 236 | return this.status; 237 | } 238 | } 239 | 240 | module.exports = Ingress; 241 | -------------------------------------------------------------------------------- /objects/namespace.js: -------------------------------------------------------------------------------- 1 | const K8Object = require('./object.js'); 2 | const { Namespace: Model } = require('../database/models.js'); 3 | const { duration } = require('../functions.js'); 4 | 5 | class Namespace extends K8Object { 6 | constructor(config) { 7 | super(config); 8 | this.spec = config.spec; 9 | this.status = config.status; 10 | } 11 | 12 | static apiVersion = 'v1'; 13 | static kind = 'Namespace'; 14 | 15 | 16 | static findOne(params = {}, options = {}) { 17 | return Model.findOne(params, options) 18 | .then((namespace) => { 19 | console.log(namespace); 20 | if (namespace) { 21 | return new Namespace(namespace).setResourceVersion(); 22 | } 23 | }); 24 | } 25 | 26 | static find(params = {}, options = {}) { 27 | return Model.find(params, options) 28 | .then((namespaces) => { 29 | if (namespaces) { 30 | return Promise.all(namespaces.map((namespace) => new Namespace(namespace).setResourceVersion())); 31 | } 32 | }); 33 | } 34 | 35 | static create(config) { 36 | return this.findOne({ 'metadata.name': config.metadata.name, 'metadata.namespace': config.metadata.namespace }) 37 | .then((existingNamespace) => { 38 | if (existingNamespace) { 39 | throw this.alreadyExistsStatus(config.metadata.name); 40 | } 41 | return new Model(config).save(); 42 | }) 43 | .then((namespace) => new Namespace(namespace)); 44 | } 45 | 46 | delete () { 47 | return Model.findOneAndDelete({ 'metadata.name': this.metadata.name }) 48 | .then((namespace) => { 49 | if (namespace) { 50 | return this.setConfig(namespace); 51 | } 52 | }); 53 | } 54 | 55 | static findAllSorted(queryOptions = {}, sortOptions = { 'created_at': 1 }) { 56 | let params = { 57 | 'metadata.namespace': queryOptions.namespace ? queryOptions.namespace : undefined, 58 | 'metadata.resourceVersion': queryOptions.resourceVersionMatch ? queryOptions.resourceVersionMatch : undefined, 59 | }; 60 | if (!([...new Set(Object.values(params))].find((e) => undefined))) { 61 | params = {}; 62 | } 63 | let projection = {}; 64 | let options = { 65 | sort: sortOptions, 66 | limit: queryOptions.limit ? Number(queryOptions.limit) : undefined, 67 | }; 68 | return this.find(params, projection, options); 69 | } 70 | 71 | static list (queryOptions = {}) { 72 | return this.findAllSorted(queryOptions) 73 | .then(async (namespaces) => ({ 74 | apiVersion: this.apiVersion, 75 | kind: `${this.kind}List`, 76 | metadata: { 77 | continue: false, 78 | remainingItemCount: queryOptions.limit && queryOptions.limit < namespaces.length ? namespaces.length - queryOptions.limit : 0, 79 | resourceVersion: `${await super.hash(`${namespaces.length}${JSON.stringify(namespaces[0])}`)}` 80 | }, 81 | items: namespaces 82 | })); 83 | } 84 | 85 | static table (queryOptions = {}) { 86 | return this.findAllSorted(queryOptions) 87 | .then(async (namespaces) => ({ 88 | "kind": "Table", 89 | "apiVersion": "meta.k8s.io/v1", 90 | "metadata": { 91 | "resourceVersion": `${await super.hash(`${namespaces.length}${JSON.stringify(namespaces[0])}`)}`, 92 | }, 93 | "columnDefinitions": [ 94 | { 95 | "name": "Name", 96 | "type": "string", 97 | "format": "name", 98 | "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names", 99 | "priority": 0 100 | }, 101 | { 102 | "name": "Age", 103 | "type": "string", 104 | "format": "", 105 | "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", 106 | "priority": 0 107 | }, 108 | ], 109 | "rows": namespaces.map((e) => ({ 110 | "cells": [ 111 | e.metadata.name, 112 | duration(new Date() - e.metadata.creationTimestamp), 113 | ], 114 | object: { 115 | "kind": "PartialObjectMetadata", 116 | "apiVersion": "meta.k8s.io/v1", 117 | metadata: e.metadata, 118 | } 119 | })), 120 | })); 121 | } 122 | 123 | static notFoundStatus(objectName = undefined) { 124 | return super.notFoundStatus(this.kind, objectName); 125 | } 126 | 127 | static forbiddenStatus(objectName = undefined) { 128 | return super.forbiddenStatus(this.kind, objectName); 129 | } 130 | 131 | static alreadyExistsStatus(objectName = undefined) { 132 | return super.alreadyExistsStatus(this.kind, objectName); 133 | } 134 | 135 | static unprocessableContentStatus(objectName = undefined, message = undefined) { 136 | return super.unprocessableContentStatus(this.kind, objectName, undefined, message); 137 | } 138 | 139 | update(updateObj, options = {}) { 140 | return Model.findOneAndUpdate( 141 | { 'metadata.name': this.metadata.name }, 142 | updateObj, 143 | { 144 | new: true, 145 | ...options, 146 | } 147 | ) 148 | .then((namespace) => { 149 | if (namespace) { 150 | return this.setConfig(namespace); 151 | } 152 | }); 153 | } 154 | 155 | async setResourceVersion() { 156 | await super.setResourceVersion(); 157 | return this; 158 | } 159 | 160 | async setConfig(config) { 161 | await super.setResourceVersion(); 162 | this.data = config.data; 163 | return this; 164 | } 165 | } 166 | 167 | module.exports = Namespace; 168 | -------------------------------------------------------------------------------- /objects/object.js: -------------------------------------------------------------------------------- 1 | const Status = require('./status.js'); 2 | 3 | class Object { 4 | constructor(config) { 5 | this.apiVersion = config.apiVersion; 6 | this.kind = config.kind; 7 | this.metadata = config.metadata; 8 | } 9 | 10 | getMetadata() { 11 | return this.metadata; 12 | } 13 | 14 | async setResourceVersion() { 15 | this.metadata = { 16 | ...this.metadata, 17 | resourceVersion: `${await Object.hash(JSON.stringify(this))}` 18 | } 19 | } 20 | 21 | getKind() { 22 | return this.kind; 23 | } 24 | 25 | getApiVersion() { 26 | return this.apiVersion; 27 | } 28 | 29 | successfulStatus() { 30 | return new Status({ 31 | status: 'Success', 32 | details: { 33 | name: this.metadata.name, 34 | kind: this.kind.toLowerCase(), 35 | uid: this.metadata.uid 36 | } 37 | }); 38 | } 39 | 40 | static arrayBufferTo53bitNumber(buffer) { 41 | const view = new DataView(buffer); 42 | const first32bits = view.getUint32(0, true); 43 | const next21bits = view.getUint32(4, true) & 0b111111111111111111111; 44 | return first32bits * 0x200000 + next21bits; 45 | } 46 | 47 | static digest256(input) { 48 | return crypto.subtle.digest("SHA-256", new TextEncoder().encode(input)); 49 | } 50 | 51 | static async hash(input) { 52 | const sha256 = await this.digest256(input); 53 | return this.arrayBufferTo53bitNumber(sha256); 54 | } 55 | 56 | static notFoundStatus(kind = undefined, name = undefined, group = undefined) { 57 | return new Status({ 58 | status: 'Failure', 59 | reason: 'NotFound', 60 | code: 404, 61 | message: kind && name ? `${kind.toLowerCase()} "${name}" not found` : undefined, 62 | details: { 63 | name, 64 | group, 65 | kind: kind ? kind.toLowerCase() : undefined, 66 | } 67 | }); 68 | } 69 | 70 | static forbiddenStatus(kind = undefined, name = undefined, group = undefined) { 71 | return new Status({ 72 | status: 'Failure', 73 | reason: 'Forbidden', 74 | code: 403, 75 | message: kind && name ? `${kind.toLowerCase()} "${name}" is forbidden: User "" cannot get resource "${name}" in API group "${group}" in the ${kind.toLowerCase()} "${name}"` : undefined, 76 | details: { 77 | name, 78 | group, 79 | kind: kind ? kind.toLowerCase() : undefined, 80 | } 81 | }); 82 | } 83 | 84 | static unprocessableContentStatus(kind = undefined, name = undefined, group = undefined, message = undefined) { 85 | return new Status({ 86 | status: 'Failure', 87 | reason: 'UnprocessableContent', 88 | code: 422, 89 | message, 90 | details: { 91 | name, 92 | group, 93 | kind: kind ? kind.toLowerCase() : undefined, 94 | } 95 | }); 96 | } 97 | 98 | static alreadyExistsStatus(kind = undefined, name = undefined, group = undefined) { 99 | return new Status({ 100 | status: 'Failure', 101 | reason: 'AlreadyExists', 102 | code: 409, 103 | message: kind && name ? `${kind.toLowerCase()} "${name}" already exists` : undefined, 104 | details: { 105 | name, 106 | group, 107 | kind: kind ? kind.toLowerCase() : undefined, 108 | } 109 | }); 110 | } 111 | 112 | static internalServerErrorStatus(kind = undefined, name = undefined, group = undefined) { 113 | return new Status({ 114 | status: 'Failure', 115 | reason: 'InternalServerError', 116 | code: 500, 117 | message: "An internal server error has occured, please see the logs for more information", 118 | details: { 119 | name, 120 | group, 121 | kind: kind ? kind.toLowerCase() : undefined, 122 | } 123 | }); 124 | } 125 | } 126 | 127 | module.exports = Object; 128 | -------------------------------------------------------------------------------- /objects/pod.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const K8Object = require('./object.js'); 3 | const Secret = require('./secret.js'); 4 | const ConfigMap = require('./configMap.js'); 5 | const { Pod: Model } = require('../database/models.js'); 6 | const { 7 | runImage, 8 | duration, 9 | stopContainer, 10 | getContainerIP, 11 | randomBytes, 12 | isContainerRunning, 13 | } = require('../functions.js'); 14 | 15 | class Pod extends K8Object { 16 | constructor(config) { 17 | super(config); 18 | this.spec = config.spec; 19 | this.status = config.status; 20 | this.eventEmitter = new EventEmitter(); 21 | } 22 | 23 | static apiVersion = 'v1'; 24 | static kind = 'Pod'; 25 | 26 | static findOne(params) { 27 | return Model.findOne(params) 28 | .then((pod) => { 29 | if (pod) { 30 | return new Pod(pod).setResourceVersion(); 31 | } 32 | }); 33 | } 34 | 35 | static find(params) { 36 | return Model.find(params) 37 | .then((pods) => { 38 | if (pods) { 39 | return Promise.all(pods.map((pod) => new Pod(pod).setResourceVersion())); 40 | } 41 | }); 42 | } 43 | 44 | static async create(config) { 45 | let otherPod = undefined; 46 | do { 47 | config.metadata.generateName = `${config.metadata.name}-${randomBytes(6).toString('hex')}`; 48 | otherPod = await Pod.findOne({ 'metadata.generateName': config.metadata.generateName }); 49 | } while (otherPod); 50 | if (!config?.metadata?.creationTimestamp) { 51 | config.metadata.creationTimestamp = new Date(); 52 | } 53 | if (!config.status) { 54 | config.status = {}; 55 | } 56 | if (!config.status.conditions) { 57 | config.status.conditions = []; 58 | } 59 | config.status.conditions.push({ 60 | type: "Initialized", 61 | status: 'True', 62 | lastTransitionTime: new Date(), 63 | }); 64 | return new Model(config).save() 65 | .then((pod) => { 66 | let newPod = new Pod(pod); 67 | return newPod.start() 68 | .then(() => new Promise((resolve, reject) => { 69 | let podIP = getContainerIP(newPod.metadata.generateName) 70 | .then((data) => JSON.parse(data.raw)[0]?.NetworkSettings.Networks.bridge.IPAddress) 71 | .then((ip) => { 72 | newPod.eventEmitter.emit('NewContainer', { 73 | ip, 74 | nodeName: '', 75 | targetRef: { 76 | kind: this.kind, 77 | namespace: newPod.metadata.namespace, 78 | name: newPod.metadata.generateName, 79 | uid: newPod.metadata.uid 80 | } 81 | }); 82 | return ip; 83 | }) 84 | let inter = setInterval(async () => { 85 | try { 86 | if ((await isContainerRunning(newPod.metadata.generateName)).object === true) { 87 | clearInterval(inter); 88 | newPod.eventEmitter.emit('ContainersReady', newPod); 89 | newPod.update({ 90 | $push: { 91 | 'status.conditions': [{ 92 | type: "ContainersReady", 93 | status: 'True', 94 | lastTransitionTime: new Date(), 95 | }] 96 | } 97 | }) 98 | .then(() => podIP.then((ip) => resolve(ip))); 99 | } 100 | } catch (e) { 101 | reject(e); 102 | } 103 | }, 1000); 104 | })) 105 | .then((podIP) => { 106 | return newPod.update({ 107 | $push: { 108 | 'status.containerStatuses': { 109 | "restartCount": 0, 110 | "started": true, 111 | "ready": true, 112 | "name": config.metadata.name, 113 | "state": { 114 | "running": { 115 | "startedAt": new Date(), 116 | } 117 | }, 118 | "imageID": "", 119 | "image": "", 120 | "lastState": {}, 121 | "containerID": newPod.metadata.generateName 122 | }, 123 | 'status.podIPs': { 124 | ip: podIP 125 | }, 126 | }, 127 | $set: { 128 | 'status.phase': 'Running', 129 | 'status.podIP': podIP, 130 | } 131 | }) 132 | }) 133 | .then((updatedPod) => { 134 | newPod.eventEmitter.emit('Ready', updatedPod); 135 | newPod.eventEmitter.emit('PodScheduled', updatedPod); 136 | return updatedPod.update({ 137 | $push: { 138 | 'status.conditions': [{ 139 | type: "Ready", 140 | status: 'True', 141 | lastTransitionTime: new Date(), 142 | }, { 143 | type: "PodScheduled", 144 | status: 'True', 145 | lastTransitionTime: new Date(), 146 | }], 147 | } 148 | }); 149 | }); 150 | }) 151 | } 152 | 153 | events() { 154 | return this.eventEmitter; 155 | } 156 | 157 | async setConfig(config) { 158 | await super.setResourceVersion(); 159 | this.spec = config.spec; 160 | this.status = config.status; 161 | return this; 162 | } 163 | 164 | async setResourceVersion() { 165 | await super.setResourceVersion(); 166 | return this; 167 | } 168 | 169 | delete () { 170 | return Model.findOneAndDelete({ 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }) 171 | .then((pod) => { 172 | if (pod) { 173 | this.eventEmitter.emit('Delete', pod); 174 | return this.setConfig(pod); 175 | } 176 | }); 177 | } 178 | 179 | update(updateObj) { 180 | return Model.findOneAndUpdate( 181 | { 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }, 182 | updateObj, 183 | { new: true } 184 | ) 185 | .then((pod) => { 186 | if (pod) { 187 | return this.setConfig(pod); 188 | } 189 | }); 190 | } 191 | 192 | static findAllSorted(queryOptions = {}, sortOptions = { 'created_at': 1 }) { 193 | let params = { 194 | 'metadata.namespace': queryOptions.namespace ? queryOptions.namespace : undefined, 195 | 'metadata.resourceVersion': queryOptions.resourceVersionMatch ? queryOptions.resourceVersionMatch : undefined, 196 | }; 197 | if (!([...new Set(Object.values(params))].find((e) => undefined))) { 198 | params = {}; 199 | } 200 | let projection = {}; 201 | let options = { 202 | sort: sortOptions, 203 | limit: queryOptions.limit ? Number(queryOptions.limit) : undefined, 204 | }; 205 | return this.find(params, projection, options); 206 | } 207 | 208 | static list (queryOptions = {}) { 209 | return this.findAllSorted(queryOptions) 210 | .then(async (pods) => ({ 211 | apiVersion: this.apiVersion, 212 | kind: `${this.kind}List`, 213 | metadata: { 214 | continue: false, 215 | remainingItemCount: queryOptions.limit && queryOptions.limit < pods.length ? pods.length - queryOptions.limit : 0, 216 | resourceVersion: `${await super.hash(`${pods.length}${JSON.stringify(pods[0])}`)}` 217 | }, 218 | items: pods 219 | })); 220 | } 221 | 222 | static table (queryOptions = {}) { 223 | return this.findAllSorted(queryOptions) 224 | .then(async (pods) => ({ 225 | "kind": "Table", 226 | "apiVersion": "meta.k8s.io/v1", 227 | "metadata": { 228 | "resourceVersion": `${await super.hash(`${pods.length}${JSON.stringify(pods[0])}`)}`, 229 | }, 230 | "columnDefinitions": [ 231 | { 232 | "name": "Name", 233 | "type": "string", 234 | "format": "name", 235 | "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names", 236 | "priority": 0 237 | }, 238 | { 239 | "name": "Ready", 240 | "type": "string", 241 | "format": "", 242 | "description": "Whether or not the pod is ready", 243 | "priority": 0 244 | }, 245 | { 246 | "name": "Status", 247 | "type": "string", 248 | "format": "", 249 | "description": "Current status of the pod.", 250 | "priority": 0 251 | }, 252 | { 253 | "name": "Restarts", 254 | "type": "string", 255 | "format": "", 256 | "description": "Number of restarts for the pod.", 257 | "priority": 0 258 | }, 259 | { 260 | "name": "Age", 261 | "type": "string", 262 | "format": "", 263 | "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", 264 | "priority": 0 265 | }, 266 | { 267 | "name": "IP", 268 | "type": "string", 269 | "format": "", 270 | "description": "IP address of the pod.", 271 | "priority": 1 272 | }, 273 | { 274 | "name": "Node", 275 | "type": "string", 276 | "format": "", 277 | "description": "Name of the node.", 278 | "priority": 1 279 | }, 280 | { 281 | "name": "Nominated Node", 282 | "type": "string", 283 | "format": "", 284 | "description": "Name of the nominated node.", 285 | "priority": 1 286 | }, 287 | { 288 | "name": "Readiness Gates", 289 | "type": "string", 290 | "format": "", 291 | "description": "Gate info.", 292 | "priority": 1 293 | } 294 | ], 295 | "rows": pods.map((e) => ({ 296 | "cells": [ 297 | e.metadata.name, 298 | `${e.status.phase === "Running" ? 1 : 0}/1`, 299 | e.status.phase, 300 | (e.status.containerStatuses.restartCount || 0), 301 | duration(new Date() - e.metadata.creationTimestamp), 302 | (e.status.podIP || ''), 303 | (e.metadata.generateName || ''), 304 | (e.status.nominatedNodeName || ''), 305 | (e.spec.readinessGates.conditionType || ''), 306 | ], 307 | object: { 308 | "kind": "PartialObjectMetadata", 309 | "apiVersion": "meta.k8s.io/v1", 310 | metadata: e.metadata, 311 | } 312 | })), 313 | })); 314 | } 315 | 316 | static notFoundStatus(objectName = undefined) { 317 | return super.notFoundStatus(this.kind, objectName); 318 | } 319 | 320 | static forbiddenStatus(objectName = undefined) { 321 | return super.forbiddenStatus(this.kind, objectName); 322 | } 323 | 324 | static alreadyExistsStatus(objectName = undefined) { 325 | return super.alreadyExistsStatus(this.kind, objectName); 326 | } 327 | 328 | static unprocessableContentStatus(objectName = undefined, message = undefined) { 329 | return super.unprocessableContentStatus(this.kind, objectName, undefined, message); 330 | } 331 | 332 | stop() { 333 | return stopContainer(this.metadata.generateName); 334 | } 335 | 336 | getEnvVarsFromSecret(secretName) { 337 | return Secret.findOne({ 'metadata.name': secretName, 'metadata.namespace': this.metadata.namespace }) 338 | .then((secret) => (secret?.mapVariables() || [])); 339 | } 340 | 341 | getEnvVarsFromConfigMaps(configNames) { 342 | return ConfigMap.find({ 343 | 'metadata.namespace': this.metadata.namespace, 344 | $or: configNames.map((e) => ({ 'metadata.name': e })), 345 | }) 346 | .then((configMaps) => { 347 | if (configMaps) { 348 | return configMaps.map((e) => ({ 349 | name: e.metadata.name, 350 | variables: e.mapVariables(), 351 | })); 352 | } 353 | return []; 354 | }); 355 | } 356 | 357 | start() { 358 | let p = this.spec.containers.map(async (e) => { 359 | let options = { 360 | expose: e.ports.map((a) => a.containerPort), 361 | } 362 | if (e.env || e.envFrom) { 363 | options['env'] = []; 364 | if (e.env) { 365 | options['env'].push(...e.env.filter((v) => v?.value)); 366 | if (e.env.find((v) => v?.valueFrom?.configMapKeyRef)) { 367 | let configMaps = (await this.getEnvVarsFromConfigMaps( 368 | e.env.map((v) => v.valueFrom.configMapKeyRef.name) 369 | )); 370 | e.env 371 | .filter((v) => v.valueFrom?.configMapKeyRef) 372 | .forEach((e) => { 373 | let value = configMaps 374 | ?.find((v) => v.name === e.valueFrom.configMapKeyRef.name) 375 | ?.variables 376 | ?.find((v) => v.name === e.valueFrom.configMapKeyRef.key) 377 | ?.value 378 | if (value) { 379 | options['env'].push({ 380 | name: e.name, 381 | value, 382 | }); 383 | } 384 | }); 385 | } 386 | } 387 | if (e.envFrom) { 388 | await Promise.all(e.envFrom.map(async (e) => { 389 | if (e.secretRef) { 390 | return this.getEnvVarsFromSecret(e.secretRef.name); 391 | } 392 | return null; 393 | })) 394 | .then((variables) => variables.flat().filter((e) => e)) 395 | .then((variables) => options['env'].push(...variables)); 396 | } 397 | } 398 | return runImage(e.image, this.metadata.generateName, options); 399 | }); 400 | return Promise.all(p); 401 | } 402 | 403 | getSpec() { 404 | return this.spec; 405 | } 406 | 407 | getStatus() { 408 | return this.status; 409 | } 410 | } 411 | 412 | module.exports = Pod; 413 | -------------------------------------------------------------------------------- /objects/role.js: -------------------------------------------------------------------------------- 1 | const K8Object = require('./object.js'); 2 | const { Role: Model } = require('../database/models.js'); 3 | const { duration } = require('../functions.js'); 4 | 5 | class Role extends K8Object { 6 | constructor(config) { 7 | super(config); 8 | this.rules = config.rules; 9 | } 10 | 11 | static apiVersion = 'v1'; 12 | static kind = 'Role'; 13 | 14 | 15 | static findOne(params = {}, options = {}) { 16 | return Model.findOne(params, options) 17 | .then((role) => { 18 | if (role) { 19 | return new Role(role).setResourceVersion(); 20 | } 21 | }); 22 | } 23 | 24 | static find(params = {}, projection = {}, queryOptions = {}) { 25 | let options = { 26 | sort: { 'metadata.name': 1 }, 27 | ...queryOptions 28 | }; 29 | return Model.find(params, projection, options) 30 | .then((roles) => { 31 | if (roles) { 32 | return Promise.all(roles.map((role) => new Role(role).setResourceVersion())); 33 | } 34 | }); 35 | } 36 | 37 | static create(config) { 38 | return this.findOne({ 'metadata.name': config.metadata.name, 'metadata.namespace': config.metadata.namespace }) 39 | .then((existingRole) => { 40 | if (existingRole) { 41 | throw this.alreadyExistsStatus(config.metadata.name); 42 | } 43 | return new Model(config).save(); 44 | }) 45 | .then((role) => new Role(role)); 46 | } 47 | 48 | delete () { 49 | return Model.findOneAndDelete({ 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }) 50 | .then((role) => { 51 | if (role) { 52 | return this.setConfig(role); 53 | } 54 | }); 55 | } 56 | 57 | static findAllSorted(queryOptions = {}, sortOptions = { 'created_at': 1 }) { 58 | let params = { 59 | 'metadata.role': queryOptions.role ? queryOptions.role : undefined, 60 | 'metadata.resourceVersion': queryOptions.resourceVersionMatch ? queryOptions.resourceVersionMatch : undefined, 61 | }; 62 | if (!([...new Set(Object.values(params))].find((e) => undefined))) { 63 | params = {}; 64 | } 65 | let projection = {}; 66 | let options = { 67 | sort: sortOptions, 68 | limit: queryOptions.limit ? Number(queryOptions.limit) : undefined, 69 | }; 70 | return this.find(params, projection, options); 71 | } 72 | 73 | static list (queryOptions = {}) { 74 | return this.findAllSorted(queryOptions) 75 | .then(async (roles) => ({ 76 | apiVersion: this.apiVersion, 77 | kind: `${this.kind}List`, 78 | metadata: { 79 | continue: false, 80 | remainingItemCount: queryOptions.limit && queryOptions.limit < roles.length ? roles.length - queryOptions.limit : 0, 81 | resourceVersion: `${await super.hash(`${roles.length}${JSON.stringify(roles[0])}`)}` 82 | }, 83 | items: roles 84 | })); 85 | } 86 | 87 | static table (queryOptions = {}) { 88 | return this.findAllSorted(queryOptions) 89 | .then(async (roles) => ({ 90 | "kind": "Table", 91 | "apiVersion": "meta.k8s.io/v1", 92 | "metadata": { 93 | "resourceVersion": `${await super.hash(`${roles.length}${JSON.stringify(roles[0])}`)}`, 94 | }, 95 | "columnDefinitions": [ 96 | { 97 | "name": "Name", 98 | "type": "string", 99 | "format": "name", 100 | "description": "Name must be unique within a role. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names", 101 | "priority": 0 102 | }, 103 | { 104 | "name": "Age", 105 | "type": "string", 106 | "format": "", 107 | "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", 108 | "priority": 0 109 | }, 110 | ], 111 | "rows": roles.map((e) => ({ 112 | "cells": [ 113 | e.metadata.name, 114 | duration(new Date() - e.metadata.creationTimestamp), 115 | ], 116 | object: { 117 | "kind": "PartialObjectMetadata", 118 | "apiVersion": "meta.k8s.io/v1", 119 | metadata: e.metadata, 120 | } 121 | })), 122 | })); 123 | } 124 | 125 | static notFoundStatus(objectName = undefined) { 126 | return super.notFoundStatus(this.kind, objectName); 127 | } 128 | 129 | static forbiddenStatus(objectName = undefined) { 130 | return super.forbiddenStatus(this.kind, objectName); 131 | } 132 | 133 | static alreadyExistsStatus(objectName = undefined) { 134 | return super.alreadyExistsStatus(this.kind, objectName); 135 | } 136 | 137 | static unprocessableContentStatus(objectName = undefined, message = undefined) { 138 | return super.unprocessableContentStatus(this.kind, objectName, undefined, message); 139 | } 140 | 141 | update(updateObj, options = {}) { 142 | return Model.findOneAndUpdate( 143 | { 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }, 144 | updateObj, 145 | { 146 | new: true, 147 | ...options, 148 | } 149 | ) 150 | .then((role) => { 151 | if (role) { 152 | return this.setConfig(role); 153 | } 154 | }); 155 | } 156 | 157 | async setResourceVersion() { 158 | await super.setResourceVersion(); 159 | return this; 160 | } 161 | 162 | async setConfig(config) { 163 | await super.setResourceVersion(); 164 | this.data = config.data; 165 | return this; 166 | } 167 | } 168 | 169 | module.exports = Role; 170 | -------------------------------------------------------------------------------- /objects/roleBinding.js: -------------------------------------------------------------------------------- 1 | const K8Object = require('./object.js'); 2 | const { RoleBinding: Model } = require('../database/models.js'); 3 | const { duration } = require('../functions.js'); 4 | 5 | class RoleBinding extends K8Object { 6 | constructor(config) { 7 | super(config); 8 | this.rules = config.rules; 9 | } 10 | 11 | static apiVersion = 'v1'; 12 | static kind = 'RoleBinding'; 13 | 14 | 15 | static findOne(params = {}, options = {}) { 16 | return Model.findOne(params, options) 17 | .then((roleBinding) => { 18 | if (roleBinding) { 19 | return new RoleBinding(roleBinding).setResourceVersion(); 20 | } 21 | }); 22 | } 23 | 24 | static find(params = {}, projection = {}, queryOptions = {}) { 25 | let options = { 26 | sort: { 'metadata.name': 1 }, 27 | ...queryOptions 28 | }; 29 | return Model.find(params, projection, options) 30 | .then((roleBindings) => { 31 | if (roleBindings) { 32 | return Promise.all(roleBindings.map((roleBinding) => new RoleBinding(roleBinding).setResourceVersion())); 33 | } 34 | }); 35 | } 36 | 37 | static create(config) { 38 | return this.findOne({ 'metadata.name': cofig.metadata.name, 'metadata.namespace': cofig.metadata.namespace }) 39 | .then((existingRoleBinding) => { 40 | if (existingRoleBinding) { 41 | throw this.alreadyExistsStatus(config.metadata.name); 42 | } 43 | return new Model(config).save(); 44 | }) 45 | .then((roleBinding) => new RoleBinding(roleBinding)); 46 | } 47 | 48 | delete () { 49 | return Model.findOneAndDelete({ 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }) 50 | .then((roleBinding) => { 51 | if (roleBinding) { 52 | return this.setConfig(roleBinding); 53 | } 54 | }); 55 | } 56 | 57 | static findAllSorted(queryOptions = {}, sortOptions = { 'created_at': 1 }) { 58 | let params = { 59 | 'metadata.roleBinding': queryOptions.roleBinding ? queryOptions.roleBinding : undefined, 60 | 'metadata.resourceVersion': queryOptions.resourceVersionMatch ? queryOptions.resourceVersionMatch : undefined, 61 | }; 62 | if (!([...new Set(Object.values(params))].find((e) => undefined))) { 63 | params = {}; 64 | } 65 | let projection = {}; 66 | let options = { 67 | sort: sortOptions, 68 | limit: queryOptions.limit ? Number(queryOptions.limit) : undefined, 69 | }; 70 | return this.find(params, projection, options); 71 | } 72 | 73 | static list (queryOptions = {}) { 74 | return this.findAllSorted(queryOptions) 75 | .then(async (roleBindings) => ({ 76 | apiVersion: this.apiVersion, 77 | kind: `${this.kind}List`, 78 | metadata: { 79 | continue: false, 80 | remainingItemCount: queryOptions.limit && queryOptions.limit < roleBindings.length ? roleBindings.length - queryOptions.limit : 0, 81 | resourceVersion: `${await super.hash(`${roleBindings.length}${JSON.stringify(roleBindings[0])}`)}` 82 | }, 83 | items: roleBindings 84 | })); 85 | } 86 | 87 | static table (queryOptions = {}) { 88 | return this.findAllSorted(queryOptions) 89 | .then(async (roleBindings) => ({ 90 | "kind": "Table", 91 | "apiVersion": "meta.k8s.io/v1", 92 | "metadata": { 93 | "resourceVersion": `${await super.hash(`${roleBindings.length}${JSON.stringify(roleBindings[0])}`)}`, 94 | }, 95 | "columnDefinitions": [ 96 | { 97 | "name": "Name", 98 | "type": "string", 99 | "format": "name", 100 | "description": "Name must be unique within a roleBinding. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names", 101 | "priority": 0 102 | }, 103 | { 104 | "name": "Age", 105 | "type": "string", 106 | "format": "", 107 | "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", 108 | "priority": 0 109 | }, 110 | ], 111 | "rows": roleBindings.map((e) => ({ 112 | "cells": [ 113 | e.metadata.name, 114 | duration(new Date() - e.metadata.creationTimestamp), 115 | ], 116 | object: { 117 | "kind": "PartialObjectMetadata", 118 | "apiVersion": "meta.k8s.io/v1", 119 | metadata: e.metadata, 120 | } 121 | })), 122 | })); 123 | } 124 | 125 | static notFoundStatus(objectName = undefined) { 126 | return super.notFoundStatus(this.kind, objectName); 127 | } 128 | 129 | static forbiddenStatus(objectName = undefined) { 130 | return super.forbiddenStatus(this.kind, objectName); 131 | } 132 | 133 | static alreadyExistsStatus(objectName = undefined) { 134 | return super.alreadyExistsStatus(this.kind, objectName); 135 | } 136 | 137 | static unprocessableContentStatus(objectName = undefined, message = undefined) { 138 | return super.unprocessableContentStatus(this.kind, objectName, undefined, message); 139 | } 140 | 141 | update(updateObj, options = {}) { 142 | return Model.findOneAndUpdate( 143 | { 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }, 144 | updateObj, 145 | { 146 | new: true, 147 | ...options, 148 | } 149 | ) 150 | .then((roleBinding) => { 151 | if (roleBinding) { 152 | return this.setConfig(roleBinding); 153 | } 154 | }); 155 | } 156 | 157 | async setResourceVersion() { 158 | await super.setResourceVersion(); 159 | return this; 160 | } 161 | 162 | async setConfig(config) { 163 | await super.setResourceVersion(); 164 | this.data = config.data; 165 | return this; 166 | } 167 | } 168 | 169 | module.exports = RoleBinding; 170 | -------------------------------------------------------------------------------- /objects/secret.js: -------------------------------------------------------------------------------- 1 | const K8Object = require('./object.js'); 2 | const { Secret: Model } = require('../database/models.js'); 3 | const { duration } = require('../functions.js'); 4 | 5 | class Secret extends K8Object { 6 | constructor(config) { 7 | super(config); 8 | this.immutable = config.immutable; 9 | this.stringData = config.stringData; 10 | this.data = config.data; 11 | this.type = config.type; 12 | } 13 | 14 | static apiVersion = 'v1'; 15 | static kind = 'Secret'; 16 | 17 | 18 | static findOne(params = {}, options = {}) { 19 | return Model.findOne(params, options) 20 | .then((secret) => { 21 | if (secret) { 22 | return new Secret(secret).setResourceVersion(); 23 | } 24 | }); 25 | } 26 | 27 | static find(params = {}, options = {}) { 28 | return Model.find(params, options) 29 | .then((secrets) => { 30 | if (secrets) { 31 | return Promise.all(secrets.map((secret) => new Secret(secret).setResourceVersion())); 32 | } 33 | }); 34 | } 35 | 36 | static create(config) { 37 | const base64RegExp = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/; 38 | const isBase64 = (str) => base64RegExp.test(str); 39 | 40 | return this.findOne({ 'metadata.name': config.metadata.name, 'metadata.namespace': config.metadata.namespace }) 41 | .then((existingSecret) => { 42 | if (existingSecret) { 43 | throw this.alreadyExistsStatus(config.metadata.name); 44 | } 45 | if (config.data) { 46 | Object.entries(config.data).forEach(([key, value]) => { 47 | if (isBase64(value)) { 48 | config.data[key] = value; 49 | return; 50 | } 51 | config.data[key] = Buffer.from(value).toString('base64'); 52 | }); 53 | } 54 | return new Model(config).save(); 55 | }) 56 | .then((secret) => new Secret(secret)); 57 | } 58 | 59 | delete () { 60 | return Model.findOneAndDelete({ 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }) 61 | .then((secret) => { 62 | if (secret) { 63 | return this.setConfig(secret); 64 | } 65 | }); 66 | } 67 | 68 | static findAllSorted(queryOptions = {}, sortOptions = { 'created_at': 1 }) { 69 | let params = { 70 | 'metadata.namespace': queryOptions.namespace ? queryOptions.namespace : undefined, 71 | 'metadata.resourceVersion': queryOptions.resourceVersionMatch ? queryOptions.resourceVersionMatch : undefined, 72 | }; 73 | if (!([...new Set(Object.values(params))].find((e) => undefined))) { 74 | params = {}; 75 | } 76 | let projection = {}; 77 | let options = { 78 | sort: sortOptions, 79 | limit: queryOptions.limit ? Number(queryOptions.limit) : undefined, 80 | }; 81 | return this.find(params, projection, options); 82 | } 83 | 84 | static list (queryOptions = {}) { 85 | return this.findAllSorted(queryOptions) 86 | .then(async (secrets) => ({ 87 | apiVersion: this.apiVersion, 88 | kind: `${this.kind}List`, 89 | metadata: { 90 | continue: false, 91 | remainingItemCount: queryOptions.limit && queryOptions.limit < secrets.length ? secrets.length - queryOptions.limit : 0, 92 | resourceVersion: `${await super.hash(`${secrets.length}${JSON.stringify(secrets[0])}`)}` 93 | }, 94 | items: secrets 95 | })); 96 | } 97 | 98 | static table (queryOptions = {}) { 99 | return this.findAllSorted(queryOptions) 100 | .then(async (secrets) => ({ 101 | "kind": "Table", 102 | "apiVersion": "meta.k8s.io/v1", 103 | "metadata": { 104 | "resourceVersion": `${await super.hash(`${secrets.length}${JSON.stringify(secrets[0])}`)}`, 105 | }, 106 | "columnDefinitions": [ 107 | { 108 | "name": "Name", 109 | "type": "string", 110 | "format": "name", 111 | "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names", 112 | "priority": 0 113 | }, 114 | { 115 | "name": "Type", 116 | "type": "string", 117 | "format": "", 118 | "description": "The type of secret", 119 | "priority": 0 120 | }, 121 | { 122 | "name": "Data", 123 | "type": "string", 124 | "format": "", 125 | "description": "Number of items in the secret", 126 | "priority": 0 127 | }, 128 | { 129 | "name": "Age", 130 | "type": "string", 131 | "format": "", 132 | "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", 133 | "priority": 0 134 | }, 135 | ], 136 | "rows": secrets.map((e) => ({ 137 | "cells": [ 138 | e.metadata.name, 139 | e.type, 140 | e.data.length, 141 | duration(new Date() - e.metadata.creationTimestamp), 142 | ], 143 | object: { 144 | "kind": "PartialObjectMetadata", 145 | "apiVersion": "meta.k8s.io/v1", 146 | metadata: e.metadata, 147 | } 148 | })), 149 | })); 150 | } 151 | 152 | static notFoundStatus(objectName = undefined) { 153 | return super.notFoundStatus(this.kind, objectName); 154 | } 155 | 156 | static forbiddenStatus(objectName = undefined) { 157 | return super.forbiddenStatus(this.kind, objectName); 158 | } 159 | 160 | static alreadyExistsStatus(objectName = undefined) { 161 | return super.alreadyExistsStatus(this.kind, objectName); 162 | } 163 | 164 | static unprocessableContentStatus(objectName = undefined, message = undefined) { 165 | return super.unprocessableContentStatus(this.kind, objectName, undefined, message); 166 | } 167 | 168 | update(updateObj, options = {}) { 169 | if (this?.immutable === true) { 170 | throw new Error(`Secret ${config.metadata.name} is immutable`); 171 | } 172 | return Model.findOneAndUpdate( 173 | { 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }, 174 | updateObj, 175 | { 176 | new: true, 177 | ...options, 178 | } 179 | ) 180 | .then((secret) => { 181 | if (secret) { 182 | return this.setConfig(secret); 183 | } 184 | }); 185 | } 186 | 187 | mapVariables() { 188 | // TODO: add other types 189 | if (this.type === 'Opaque') { 190 | return [ 191 | [...this.data] 192 | .map(([key, value]) => ({ 193 | name: key, 194 | value: Buffer.from(value, 'base64').toString(), 195 | })), 196 | [...this.stringData] 197 | .map(([key, value]) => ({ 198 | name: key, 199 | value, 200 | })) 201 | ].flat(); 202 | } 203 | return null; 204 | } 205 | 206 | async setResourceVersion() { 207 | await super.setResourceVersion(); 208 | return this; 209 | } 210 | 211 | async setConfig(config) { 212 | await super.setResourceVersion(); 213 | this.data = config.data; 214 | return this; 215 | } 216 | } 217 | 218 | module.exports = Secret; 219 | -------------------------------------------------------------------------------- /objects/service.js: -------------------------------------------------------------------------------- 1 | const K8Object = require('./object.js'); 2 | const Pod = require('./pod.js'); 3 | const Endpoints = require('./endpoints.js'); 4 | const { Service: Model, DNS } = require('../database/models.js'); 5 | const { 6 | addPortsToService, 7 | addPortToService, 8 | addPodToService, 9 | removePortFromService, 10 | removePodFromService, 11 | pullImage, 12 | imageExists, 13 | buildImage, 14 | runImage, 15 | getContainerIP, 16 | duration, 17 | } = require('../functions.js'); 18 | 19 | class Service extends K8Object { 20 | constructor(config) { 21 | super(config); 22 | this.spec = config.spec; 23 | this.status = config.status; 24 | this.endpoints = null; 25 | } 26 | 27 | static apiVersion = 'v1'; 28 | static kind = 'Service'; 29 | 30 | static findOne(params) { 31 | return Model.findOne(params) 32 | .then((service) => { 33 | if (service) { 34 | Endpoints.findOne(params) 35 | .then((endpoints) => { 36 | return new Service({ 37 | ...service, 38 | endpoints, 39 | }).setResourceVersion(); 40 | }) 41 | } 42 | }); 43 | } 44 | 45 | static find(params) { 46 | return Model.find(params) 47 | .then((services) => { 48 | if (services) { 49 | Endpoints.find(params) 50 | .then((endpoints) => { 51 | return Promise.all(services.map((service) => new Service({ 52 | ...service, 53 | endpoints: endpoints.find((e) => e.metadata.name === service.metadata.name), 54 | }).setResourceVersion())); 55 | }) 56 | } 57 | }); 58 | } 59 | 60 | convertToSubsets(pods) { 61 | let mappedIPs = [] 62 | return [{ 63 | notReadyAddresses: pods.map((p) => { 64 | return p.status.podIPs.map((podIP) => { 65 | if (!mappedIPs.includes(podIP.ip)) { 66 | mappedIPs.push(podIP.ip) 67 | return { 68 | ip: podIP.ip, 69 | nodeName: '', 70 | targetRef: { 71 | kind: 'Pod', 72 | namespace: this.metadata.namespace, 73 | name: p.metadata.generateName, 74 | uid: p.metadata.uid, 75 | } 76 | }; 77 | } 78 | }) 79 | }).flat().filter((e) => e), 80 | ports: this.spec.ports.map((p) => ({ 81 | name: p.name, 82 | port: p.targetPort, 83 | protocol: p.protocol, 84 | })) 85 | }]; 86 | } 87 | 88 | static create(config) { 89 | return Promise.all([ 90 | this.findOne({ 'metadata.name': config.metadata.name, 'metadata.namespace': config.metadata.namespace }), 91 | Endpoints.findOne({ 'metadata.name': config.metadata.name, 'metadata.namespace': config.metadata.namespace }) 92 | ]) 93 | .then((data) => { 94 | let [ existingService, existingEndpoint ] = data; 95 | if (existingService) { 96 | throw this.alreadyExistsStatus(config.metadata.name); 97 | } 98 | if (existingEndpoint) { 99 | throw Endpoints.alreadyExistsStatus(config.metadata.name); 100 | } 101 | return new Model(config).save() 102 | }) 103 | .then((service) => { 104 | let newService = new Service(service); 105 | return Pod.find({ 'metadata.name': config.metadata.name, 'metadata.namespace': config.metadata.namespace }) 106 | .then((pods) => { 107 | return Endpoints.create({ 108 | metadata: newService.metadata, 109 | subsets: newService.convertToSubsets(pods), 110 | ports: newService.spec.ports.map((e) => `${e.port}:${e.targetPort}`).join(' '), 111 | }) 112 | .then((newEndpoints) => { 113 | newService.endpoints = newEndpoints; 114 | return getContainerIP(`${newService.endpoints.metadata.name}-loadBalancer`); 115 | }) 116 | .then((ipInfo) => JSON.parse(ipInfo?.raw)[0]?.NetworkSettings?.Networks?.bridge?.IPAddress) 117 | .then((serviceIP) => { 118 | if (serviceIP) { 119 | return newService.update({ 120 | $set: { 121 | 'spec.clusterIP': serviceIP, 122 | 'spec.clusterIPs': [serviceIP], 123 | } 124 | }); 125 | } 126 | }) 127 | .then(async () => { 128 | await new DNS({ 129 | name: `${newService.metadata.name}.${newService.metadata.namespace}.cluster.local`, 130 | type: 'A', 131 | class: 'IN', 132 | ttl: 300, 133 | address: newService.spec.clusterIP, 134 | }).save() 135 | return newService; 136 | }); 137 | }) 138 | }); 139 | } 140 | 141 | delete () { 142 | return Model.findOneAndDelete({ 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }) 143 | .then((service) => { 144 | if (service) { 145 | return this.setConfig(service); 146 | } 147 | }); 148 | } 149 | 150 | update(updateObj) { 151 | return Model.findOneAndUpdate( 152 | { 'metadata.name': this.metadata.name, 'metadata.namespace': this.metadata.namespace }, 153 | updateObj, 154 | { new: true } 155 | ) 156 | .then((service) => { 157 | if (service) { 158 | return this.setConfig(service); 159 | } 160 | }); 161 | } 162 | 163 | static notFoundStatus(objectName = undefined) { 164 | return super.notFoundStatus(this.kind, objectName); 165 | } 166 | 167 | static forbiddenStatus(objectName = undefined) { 168 | return super.forbiddenStatus(this.kind, objectName); 169 | } 170 | 171 | static alreadyExistsStatus(objectName = undefined) { 172 | return super.alreadyExistsStatus(this.kind, objectName); 173 | } 174 | 175 | static unprocessableContentStatus(objectName = undefined, message = undefined) { 176 | return super.unprocessableContentStatus(this.kind, objectName, undefined, message); 177 | } 178 | 179 | static findAllSorted(queryOptions = {}, sortOptions = { 'created_at': 1 }) { 180 | let params = { 181 | 'metadata.namespace': queryOptions.namespace ? queryOptions.namespace : undefined, 182 | 'metadata.resourceVersion': queryOptions.resourceVersionMatch ? queryOptions.resourceVersionMatch : undefined, 183 | }; 184 | if (!([...new Set(Object.values(params))].find((e) => undefined))) { 185 | params = {}; 186 | } 187 | let projection = {}; 188 | let options = { 189 | sort: sortOptions, 190 | limit: queryOptions.limit ? Number(queryOptions.limit) : undefined, 191 | }; 192 | return this.find(params, projection, options); 193 | } 194 | 195 | static list (queryOptions = {}) { 196 | return this.findAllSorted(queryOptions) 197 | .then(async (services) => ({ 198 | apiVersion: this.apiVersion, 199 | kind: `${this.kind}List`, 200 | metadata: { 201 | continue: false, 202 | remainingItemCount: queryOptions.limit && queryOptions.limit < services.length ? services.length - queryOptions.limit : 0, 203 | resourceVersion: `${await super.hash(`${services.length}${JSON.stringify(services[0])}`)}` 204 | }, 205 | items: services 206 | })); 207 | } 208 | 209 | static table (queryOptions = {}) { 210 | return this.findAllSorted(queryOptions) 211 | .then(async (services) => ({ 212 | "kind": "Table", 213 | "apiVersion": "meta.k8s.io/v1", 214 | "metadata": { 215 | "resourceVersion": `${await super.hash(`${services.length}${JSON.stringify(services[0])}`)}`, 216 | }, 217 | "columnDefinitions": [ 218 | { 219 | "name": "Name", 220 | "type": "string", 221 | "format": "name", 222 | "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names", 223 | "priority": 0 224 | }, 225 | { 226 | "name": "Type", 227 | "type": "string", 228 | "format": "name", 229 | "description": "Type of service.", 230 | "priority": 0 231 | }, 232 | { 233 | "name": "Cluster-ip", 234 | "type": "string", 235 | "format": "", 236 | "description": "IP within the cluster.", 237 | "priority": 0 238 | }, 239 | { 240 | "name": "External-ip", 241 | "type": "string", 242 | "format": "", 243 | "description": "IP outside the cluster.", 244 | "priority": 0 245 | }, 246 | { 247 | "name": "Port(s)", 248 | "type": "string", 249 | "format": "", 250 | "description": "Port(s) exposed by the service, for the pod(s).", 251 | "priority": 0 252 | }, 253 | { 254 | "name": "Age", 255 | "type": "string", 256 | "format": "", 257 | "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", 258 | "priority": 0 259 | }, 260 | { 261 | "name": "Selector", 262 | "type": "string", 263 | "format": "", 264 | "description": "Which pod(s) are fronted by the service.", 265 | "priority": 1 266 | } 267 | ], 268 | "rows": pods.map((e) => ({ 269 | "cells": [ 270 | e.metadata.name, 271 | e.spec.type, 272 | (e.spec.clusterIP || e.spec.clusterIPs?.join() || ''), 273 | (e.spec.externalIPs?.join() || ''), 274 | e.spec?.ports?.length > 0 ? e.spec.ports.map((e) => `${e.port}/${e.protocol}`).join() : '', 275 | duration(new Date() - e.metadata.creationTimestamp), 276 | e.spec?.selector && Object.keys(e.spec.selector).length > 0 ? Object.entries(e.spec.selector).map((e) => `${e[0]}=${e[1]}`).join() : '', 277 | ], 278 | object: { 279 | "kind": "PartialObjectMetadata", 280 | "apiVersion": "meta.k8s.io/v1", 281 | metadata: e.metadata, 282 | } 283 | })), 284 | })); 285 | } 286 | 287 | async setConfig(config) { 288 | await super.setResourceVersion(); 289 | this.spec = config.spec; 290 | this.status = config.status; 291 | return this; 292 | } 293 | 294 | async setResourceVersion() { 295 | await super.setResourceVersion(); 296 | return this; 297 | } 298 | 299 | getSpec() { 300 | return this.spec; 301 | } 302 | 303 | getStatus() { 304 | return this.status; 305 | } 306 | 307 | removePort(port) { 308 | return removePortFromService(`${this.endpoints.metadata.generateName}-loadBalancer`, port); 309 | } 310 | 311 | addPorts(ports) { 312 | return addPortsToService(`${this.endpoints.metadata.generateName}-loadBalancer`, ports); 313 | } 314 | 315 | addPort(port) { 316 | return addPortToService(`${this.endpoints.metadata.generateName}-loadBalancer`, port); 317 | } 318 | 319 | removePod(pod) { 320 | return this.endpoints.removePod(pod); 321 | } 322 | 323 | addPod(pod) { 324 | return this.endpoints.addPod(pod); 325 | } 326 | } 327 | 328 | module.exports = Service; 329 | -------------------------------------------------------------------------------- /objects/status.js: -------------------------------------------------------------------------------- 1 | class Status { 2 | constructor(config) { 3 | this.kind = "Status"; 4 | this.apiVersion = "v1"; 5 | this.metadata = {}; 6 | this.code = config.code; 7 | this.message = config.message; 8 | this.reason = config.reason; 9 | this.status = config.status; 10 | this.details = config.details; 11 | } 12 | 13 | setConfig(config) { 14 | this.code = config.code; 15 | this.message = config.message; 16 | this.reason = config.reason; 17 | this.status = config.status; 18 | this.details = config.details; 19 | return this; 20 | } 21 | 22 | setStatus(status) { 23 | this.status = status; 24 | return this; 25 | } 26 | 27 | setMessage(message) { 28 | this.message = message; 29 | return this; 30 | } 31 | 32 | setReason(reason) { 33 | this.reason = reason; 34 | return this; 35 | } 36 | 37 | setDetails(details) { 38 | this.details = details; 39 | return this; 40 | } 41 | 42 | getSpec() { 43 | return this.spec; 44 | } 45 | 46 | getStatus() { 47 | return this.status; 48 | } 49 | } 50 | 51 | module.exports = Status; 52 | -------------------------------------------------------------------------------- /openApiSpecs/v3/api.json: -------------------------------------------------------------------------------- 1 | { 2 | "paths": { 3 | ".well-known/openid-configuration": { 4 | "serverRelativeURL": "/openapi/v3/.well-known/openid-configuration?hash=B0310CBEA669D29AFA3D06473E3C0FDFF9D8AE1BBE1A02BD4127155E936ECB29E5004D4C0931F08C67BC7F5A5FECF9094E4911599E23863ED00D4BE81BE65322" 5 | }, 6 | "api": { 7 | "serverRelativeURL": "/openapi/v3/api?hash=B550C3CAECBA12530D63A08D6C9BCBFEB67DB58202773A39FCCDFC87FF4F6333EB8F6B148B274E5A2D5CC930C0DE78416124E5C32B6AAC90A6520475BAF7EDB5" 8 | }, 9 | "api/v1": { 10 | "serverRelativeURL": "/openapi/v3/api/v1?hash=6D63AEA33F7652B389AB2D3FB4412DAA64005795BD407F1A7EC08E15E6FA1C03C17A2A1BE651E458644D21114D7F3C30B6B163FF89A39FC327E292F437832DE9" 11 | }, 12 | "apis": { 13 | "serverRelativeURL": "/openapi/v3/apis?hash=A09654A5DC681194939D57296F49B7C1B5B0133C64C59AD2A4F073F38632A7AEE89E53E383F32BE7ACD0D4E1DB6525912757DEBFE17622A30E3A209F83D3E374" 14 | }, 15 | "apis/acme.cert-manager.io/v1": { 16 | "serverRelativeURL": "/openapi/v3/apis/acme.cert-manager.io/v1?hash=D2103A83C6CC1B60C21DFC404B83F8A95FB0A5CAC4A3240F0634D5023F996203755AE1E426A46A4EF7783436066A4753DB324FC72359DFF1915DAE71FC309B9D" 17 | }, 18 | "apis/admissionregistration.k8s.io": { 19 | "serverRelativeURL": "/openapi/v3/apis/admissionregistration.k8s.io?hash=EBC016B55DA917877E9EA988BE60BB80BF19F34D25AF70A4B358F8E9CECBB2846B02C09FBEB1EF2B3A58AF7A21EED6A4180D701790AA7D3D068F0F2014F5CA35" 20 | }, 21 | "apis/admissionregistration.k8s.io/v1": { 22 | "serverRelativeURL": "/openapi/v3/apis/admissionregistration.k8s.io/v1?hash=4FB086C76F9793C64450ED65B18AE82DA00D5E956F954A0FF6E34B7BDB44FF979904314BED0BA071C9837C8BBF2D9D905CA8E9031AC96832C8210EF1901E1633" 23 | }, 24 | "apis/apiextensions.k8s.io": { 25 | "serverRelativeURL": "/openapi/v3/apis/apiextensions.k8s.io?hash=7CE07EF8101322A92B0DFACC1FF955010228FA9A307E8C1635546783D91CC46F917276CF7B73485074E715F050C481914AB245B99E5BED1613BDD94E4F76750B" 26 | }, 27 | "apis/apiextensions.k8s.io/v1": { 28 | "serverRelativeURL": "/openapi/v3/apis/apiextensions.k8s.io/v1?hash=56742E899203683FB7010F61CB7B9C426FAE7AF1991FF50D1D8EAC0F0087E916317A88A71EE41F03BB7447DF4C223ACBA2BA4E0AB7DB5856555A0933991DC989" 29 | }, 30 | "apis/apiserver.openshift.io/v1": { 31 | "serverRelativeURL": "/openapi/v3/apis/apiserver.openshift.io/v1?hash=FD75C5E422567FD92B879C9325259849989E5956A174D6DEDE8C78515B0A15E8553A5E341C085A17547FB166110C79C7DE3116E18FBB6496AD3B25635133F0D4" 32 | }, 33 | "apis/app.k8s.io/v1beta1": { 34 | "serverRelativeURL": "/openapi/v3/apis/app.k8s.io/v1beta1?hash=C6D3D6FB63FDA970CD1095ABF3CB0904B5005C9AC7A14E3F7347BCB85373FE5D915630F618D844AB0286E982522F25AD3DFE08277E0196995270B70573A76C55" 35 | }, 36 | "apis/apps": { 37 | "serverRelativeURL": "/openapi/v3/apis/apps?hash=3725C8DA7883F0D3A0F66BC7CBDD1FE3C4DF39317E30270DA950D59809C5ED6DD49D07A096DF7251F1D1B1C65A2BDB0014FFEF941581054A9B7BC9D8B54B8E07" 38 | }, 39 | "apis/apps.open-cluster-management.io/v1": { 40 | "serverRelativeURL": "/openapi/v3/apis/apps.open-cluster-management.io/v1?hash=714B3AC658A90B3C4A5B20D946CD144524A1C81C22F53BA38623986A0BAFA8BB879EC0A6A77434A475A8940DBBDE3B356888B527910C456EF8B22274F4E267FE" 41 | }, 42 | "apis/apps.open-cluster-management.io/v1beta1": { 43 | "serverRelativeURL": "/openapi/v3/apis/apps.open-cluster-management.io/v1beta1?hash=E675B59F7678973460E4D36B136529D4508BD1C4E7AA32185021A9D25959AB25BF224B08EB08E46339B57438928067263E804138620E7B028508E1337F8ADECC" 44 | }, 45 | "apis/apps.openshift.io/v1": { 46 | "serverRelativeURL": "/openapi/v3/apis/apps.openshift.io/v1?hash=392A0E121263F6A3D1F286A9F30532A7E3A2EFD37E252BAA39ED1E22CC97C10885AA2C0E17FA002584CE77CADA639618391661638F08B636F132968416EC2FB8" 47 | }, 48 | "apis/apps/v1": { 49 | "serverRelativeURL": "/openapi/v3/apis/apps/v1?hash=502BE2196D969FC9AA62970F7C9BFD61440701C0559D22DB27DDD5E0A94F5AC575F1B46473EC98C1489F61032344980417650DD7F9ABBA7A82E5BD9F1C40738E" 50 | }, 51 | "apis/authentication.k8s.io": { 52 | "serverRelativeURL": "/openapi/v3/apis/authentication.k8s.io?hash=65E8BC339934C86522A2E63CE70C17120B6EAD0ECA24FCBB01F961FFA25B1A1FE81EFB18A8DDB9FF6145DCB9412706BF29A685E355A69948F51247E86AA290CB" 53 | }, 54 | "apis/authentication.k8s.io/v1": { 55 | "serverRelativeURL": "/openapi/v3/apis/authentication.k8s.io/v1?hash=E83460B4FE12841A7B0FBDCD0BB8A87B900FD3D53A9EB1500AB8864F1E365BE53F62359FDABBFA7CF008B21EDF2D74862940FE48F3BD8865A4740F3184B42943" 56 | }, 57 | "apis/authorization.k8s.io": { 58 | "serverRelativeURL": "/openapi/v3/apis/authorization.k8s.io?hash=E2E1BDB8E9CF095AC6BDAFB3371B50814431CA2EAB6356601AAD1F06C73113955EB189D2E847EE963542918904045B8870C8B02603B0F7D89381745F7F4E5E55" 59 | }, 60 | "apis/authorization.k8s.io/v1": { 61 | "serverRelativeURL": "/openapi/v3/apis/authorization.k8s.io/v1?hash=3DFCDD5063B32023E891C5BC60893707C6F80A2284307AC8DB37029203900B8DC527B76134A14D28B425646682AAA7C47B556D24D0B044B30E4510369B3C639A" 62 | }, 63 | "apis/authorization.openshift.io/v1": { 64 | "serverRelativeURL": "/openapi/v3/apis/authorization.openshift.io/v1?hash=D7DAA4A4AF06ECFBBE6CC92A6CC7C99F44281CBAB9B7B49B69FBB33AE29970A464D6EA954C7122FCBEE3AAE342ADD5E02FCFD1F3AF5B5ED71AA955399718807D" 65 | }, 66 | "apis/autopilot.libopenstorage.org/v1alpha1": { 67 | "serverRelativeURL": "/openapi/v3/apis/autopilot.libopenstorage.org/v1alpha1?hash=091DEBBD0C136445B3989ED0EA6626C55809706F06E7AABA845F49D7C9F912AE5683EF6D0340993C9F42DE9BFA8DBA98D9D6B13BDBD28BE05E33D9673629028C" 68 | }, 69 | "apis/autoscaling": { 70 | "serverRelativeURL": "/openapi/v3/apis/autoscaling?hash=277A3BF459D2CC85EA13121ED546A21D5C06505B5FF2469E7C839E5437E8069C91F7E5C1B8D9BE3FB2E13EB247DD61E09392DA133B7CA4BC941A5B9F3E9EE729" 71 | }, 72 | "apis/autoscaling.openshift.io/v1": { 73 | "serverRelativeURL": "/openapi/v3/apis/autoscaling.openshift.io/v1?hash=7A08DEAA8C3D25773A2226207EB2C9477A8E53674C9C29F4F23EFDCA47A98207D3D25F7FEDCB39282830637FBDACC2BF2F4DF85AED72F54E46AB69F1D653ECC1" 74 | }, 75 | "apis/autoscaling.openshift.io/v1beta1": { 76 | "serverRelativeURL": "/openapi/v3/apis/autoscaling.openshift.io/v1beta1?hash=980175E0C1552C496C4BA947BD91456633E4A54025DF6EC93D2EF44A7AAA32CB71703055C96320409DF2C003D944A6FD60FE6A6E0F96A78E09FF84C7F26DCE10" 77 | }, 78 | "apis/autoscaling/v1": { 79 | "serverRelativeURL": "/openapi/v3/apis/autoscaling/v1?hash=4997575A0D9B31BC473E0FE12D60BAFAA4D1214F45A9283D88084CC70E3DD8005A32DFAFDFBEC4EB3C6700E32869EE19CDDE5CB5485FAFBC34E49165C41F14D4" 80 | }, 81 | "apis/autoscaling/v2": { 82 | "serverRelativeURL": "/openapi/v3/apis/autoscaling/v2?hash=9A83A98BDC8298047FA57D844CD764B8848BAA7E4A237753CF452A33A1F009D9955ADBDFBF087D8FD9F76B6303AB85826D33411B68E1C334588FEB57D2B2F658" 83 | }, 84 | "apis/awx.ansible.com/v1beta1": { 85 | "serverRelativeURL": "/openapi/v3/apis/awx.ansible.com/v1beta1?hash=7C561F278C0B86550C3F6B165E01ED989FC79924FB3E8AE57377319174F5B4CFB312E0B65F3D0AE0DBF1D12F037D1A83C9DE21647EFFC82786FB17329687CFB0" 86 | }, 87 | "apis/batch": { 88 | "serverRelativeURL": "/openapi/v3/apis/batch?hash=2875F11F9A5BFA57B0A6544C2A248F15C157C6F7227FBF4B5F3EC14C0345896DDDC638DD12D12316A6926C4E4B420C98EAC33066C86EC2F8E6136171A30D1BD9" 89 | }, 90 | "apis/batch/v1": { 91 | "serverRelativeURL": "/openapi/v3/apis/batch/v1?hash=AFFAE77D08BF278E2E3C0A1D36C7D4A30552529E7F40F7C571C903534893979FDB1F29D727866F06B1E06F15ED5C207FE8DF8DBCBDE2D86B599A89856D51A602" 92 | }, 93 | "apis/build.openshift.io/v1": { 94 | "serverRelativeURL": "/openapi/v3/apis/build.openshift.io/v1?hash=392A0E121263F6A3D1F286A9F30532A7E3A2EFD37E252BAA39ED1E22CC97C10885AA2C0E17FA002584CE77CADA639618391661638F08B636F132968416EC2FB8" 95 | }, 96 | "apis/cert-manager.io/v1": { 97 | "serverRelativeURL": "/openapi/v3/apis/cert-manager.io/v1?hash=2E3BD740A363D66C4B42AE12FB7388EA74847AA610A0F41EDF2F80720250B5BF65A5EA43362DFB5155AFA22705C142698437B24F10A43884C5EAFA0C86E7BF74" 98 | }, 99 | "apis/certificates.k8s.io": { 100 | "serverRelativeURL": "/openapi/v3/apis/certificates.k8s.io?hash=871C2112B8F9564D401094FE19016988CC2EF024447D989E7EF3706E99559E91FECE56E9BC57D37E276F00BD773B1653D68DE00A7AC6410A7255D7C94BAE5B16" 101 | }, 102 | "apis/certificates.k8s.io/v1": { 103 | "serverRelativeURL": "/openapi/v3/apis/certificates.k8s.io/v1?hash=040D4D7BDD2B4646CD9E82ECDA5FAE3F829C77BE45704576F7E3E00DD22113BAEBFA3BAF13F24521A6862C53AC50D5F9A67BAA93A22FA88C7F4D86ED6DF6EC34" 104 | }, 105 | "apis/citrix.com/v1alpha1": { 106 | "serverRelativeURL": "/openapi/v3/apis/citrix.com/v1alpha1?hash=6F7EBFF338CDEFA876DBF3A8CE65541F683F529138CD59313AED9916A91A4D4B65F3962E9E322DA30E2A7BC1F2E048D05935E0B7E1E71CA4C18DD16604A93E46" 107 | }, 108 | "apis/citrix.com/v1beta1": { 109 | "serverRelativeURL": "/openapi/v3/apis/citrix.com/v1beta1?hash=8A2A38C699F0335D60F53E9154BD03E68EE1697DAAB17BDA21AF6D1FA317202E57CB4968C98A7F5E052E1C681ADA6A6999F6B8319B6EC3AEBC341CCA33E30888" 110 | }, 111 | "apis/cloudcredential.openshift.io/v1": { 112 | "serverRelativeURL": "/openapi/v3/apis/cloudcredential.openshift.io/v1?hash=F9FCBEAAE5C77FA68B3A81EEAF9A9013B5AEEC2278BF8A1A181F573411B9A87823CBBEFE0A41C046EBD88E9DC3B9D3082E63A806D8DE557C7F2E68598233A071" 113 | }, 114 | "apis/cluster.open-cluster-management.io/v1alpha1": { 115 | "serverRelativeURL": "/openapi/v3/apis/cluster.open-cluster-management.io/v1alpha1?hash=78E5F3157B83A1DD6DCB77BEEE62C72C97A19897CEE7C6D7B5472928D3D86BABA2A429A926B20CEBE777713ED24BD02CA5E78DCDC29673014D34C51C7E08BC4B" 116 | }, 117 | "apis/cluster.open-cluster-management.io/v1beta1": { 118 | "serverRelativeURL": "/openapi/v3/apis/cluster.open-cluster-management.io/v1beta1?hash=4DDA0654D385E1AC0747EE9D8A5428B7063AC370CFF737BBEB81131F17C0331362B4314645995761CC9B8E2075C81D2BEE66482566FC49C8CC157BB380DA0899" 119 | }, 120 | "apis/cns.vmware.com/v1alpha1": { 121 | "serverRelativeURL": "/openapi/v3/apis/cns.vmware.com/v1alpha1?hash=6E40E0EC10B005C1E39AC725033B2B0B0A6DEC23BEC9523B45433AF2288CFD02C72A53EF6EEC06B5441B7BDA8FE649744D9DC41A97F4D42394AB238B6AB96E7E" 122 | }, 123 | "apis/config.openshift.io/v1": { 124 | "serverRelativeURL": "/openapi/v3/apis/config.openshift.io/v1?hash=75FE9775084B8529CB3304512723E2074DA2079FFCC9F7C9D9C9793C14523E8AEE5CBBBF9570DD406DE229C0D415AB6B5DA3A895F552F4E4A158710693940D1C" 125 | }, 126 | "apis/config.openshift.io/v1alpha1": { 127 | "serverRelativeURL": "/openapi/v3/apis/config.openshift.io/v1alpha1?hash=EA6E4DA6037E905650EDF2492F05EDA5DC411A81F71A121927922C61760252DC8FF8A830F12A3E89765F0603F78C19DBCC58F380BB128F50EDBDB9AE425890CC" 128 | }, 129 | "apis/console.openshift.io/v1": { 130 | "serverRelativeURL": "/openapi/v3/apis/console.openshift.io/v1?hash=2CFAFF45BF3D1E6044B7B88D0FDC8EF4F17798E990CE8948142FD77C581722060B610D15CEAED2030D5ECE534E0F9C3C38D9CE9325FE55AE78DADF03A6454227" 131 | }, 132 | "apis/console.openshift.io/v1alpha1": { 133 | "serverRelativeURL": "/openapi/v3/apis/console.openshift.io/v1alpha1?hash=EA79D1B1FA500D2C0F48D1412748F1CED5863C26FCA59D0355A4ADCA726EC435E87E8AF762ABCB1475E11A81DAAD1309F362CA2721CA9F3A7DAC21533CEB6557" 134 | }, 135 | "apis/controlplane.operator.openshift.io/v1alpha1": { 136 | "serverRelativeURL": "/openapi/v3/apis/controlplane.operator.openshift.io/v1alpha1?hash=CDABCF85D0A91984A7C58D1D4B7E2308300482C463D6D60FF678B4EA903CB78C36ACC21B800E982FEE262C373327D4EBAFEDBB4D95C458843FB332372033B0A0" 137 | }, 138 | "apis/coordination.k8s.io": { 139 | "serverRelativeURL": "/openapi/v3/apis/coordination.k8s.io?hash=A2F0DDDEC54BBA5F6794463255F621E29099F941779E4AA219AB9E36B6CDB824EA85DB07221C540DD88AD322B8802A6F8C53352EDFD08E676A69504BCF6D1C4F" 140 | }, 141 | "apis/coordination.k8s.io/v1": { 142 | "serverRelativeURL": "/openapi/v3/apis/coordination.k8s.io/v1?hash=8A09CAFE5FEAA66C47FAFAB42C15373079BEACC95CAEED260DF96071D711FA564ED9CC92FCC57FCE15EB9CA5C0D8A9969F0593BCED7BA744DA9657CC5890FFEA" 143 | }, 144 | "apis/core.libopenstorage.org/v1": { 145 | "serverRelativeURL": "/openapi/v3/apis/core.libopenstorage.org/v1?hash=72D438B26595D725336B3A881942300E9B8805401163E367AB04268C9CB608D4D047A72050B4B2FCBB5CF36DD562EB9667C8A61CF559C68402C222FAAA357B3F" 146 | }, 147 | "apis/core.observatorium.io/v1alpha1": { 148 | "serverRelativeURL": "/openapi/v3/apis/core.observatorium.io/v1alpha1?hash=7481818C688F036C3B8ED6E2210D477E24420FE5AEB0C533AA3DFEE81DDB768BC23BFF3166CD89D6DECF64155CCACFF63AB0145B0E263F3B6DF7A28286DC46BB" 149 | }, 150 | "apis/discovery.k8s.io": { 151 | "serverRelativeURL": "/openapi/v3/apis/discovery.k8s.io?hash=318F0AE49F5943B295AF8057361BD1183D4F5393FBA7F0EB7D5C149741AA7CF5EC51D7963337AAD76F5762FA793417A8B2388ACFBD6440D93B3C1D0740319484" 152 | }, 153 | "apis/discovery.k8s.io/v1": { 154 | "serverRelativeURL": "/openapi/v3/apis/discovery.k8s.io/v1?hash=59877ED73D1C8076099EDE19BBB5FD239C99CF2F0EF9594708A0123E6951046CD1745763DEF95A8228A20498A9C6B03B388AA7E6F258A4E63831CB9E731F70FF" 155 | }, 156 | "apis/events.k8s.io": { 157 | "serverRelativeURL": "/openapi/v3/apis/events.k8s.io?hash=CB3AA11EA717E33EA65B28A8BD9AB0CEBD17C6569A6F1088F0997A40D05437AE9F90DB65FCD08161826C5076C01E903F37111C82EBEC2614E130F23A12F4936C" 158 | }, 159 | "apis/events.k8s.io/v1": { 160 | "serverRelativeURL": "/openapi/v3/apis/events.k8s.io/v1?hash=7E6B59C3664DD133B92929DCD682B19FAD01E5B955DA68094CEDCB2A27C12C173CA470302B246E42222A1C3A445DD89A21C6F48C02BE9A85775F38381DFA4A81" 161 | }, 162 | "apis/flowcontrol.apiserver.k8s.io": { 163 | "serverRelativeURL": "/openapi/v3/apis/flowcontrol.apiserver.k8s.io?hash=B5E3FF82B68846E7644F4FB403B64DF2385E19AFAEB606C98037CC7ACAA638BE416892E30658443D2AA35A0384C84C01DB92F205201179B5272279DC8550C1C8" 164 | }, 165 | "apis/flowcontrol.apiserver.k8s.io/v1beta2": { 166 | "serverRelativeURL": "/openapi/v3/apis/flowcontrol.apiserver.k8s.io/v1beta2?hash=67DF4810055A44092FD3C89FC39F31627C70B95F0261EE414E230795F5572ED665A279E73911054333BF4CFD232BE738191C0F6A1857B540E8F6328C791C88B5" 167 | }, 168 | "apis/flowcontrol.apiserver.k8s.io/v1beta3": { 169 | "serverRelativeURL": "/openapi/v3/apis/flowcontrol.apiserver.k8s.io/v1beta3?hash=BCD843E02EB14354DBD5122BDA18C7B74FB1ECFCC64FFADEEA6E95749EF8BF3A356521DFF69417E2436E75FFDCF8BF665CD77F33214F9D9B28D49DB8AA238B73" 170 | }, 171 | "apis/helm.openshift.io/v1beta1": { 172 | "serverRelativeURL": "/openapi/v3/apis/helm.openshift.io/v1beta1?hash=8F43A9D3D6BC1005BFC696C35E0E36F3840B88EB2026379BF13FE9BE3477C61DC7CCC6D56FE5E2DC1B8D51F47300BA1D9E0925A3E16AD417A8136691CB7616BB" 173 | }, 174 | "apis/hive.openshift.io/v1": { 175 | "serverRelativeURL": "/openapi/v3/apis/hive.openshift.io/v1?hash=DF5C4E318A1643C369EAFC8891B28AE6B62FD79A016570A5C39E3186BDD409553A241A2213006206927106A21DB66C19566B0EDD409BD83832DB0DB3E4E3D363" 176 | }, 177 | "apis/hiveinternal.openshift.io/v1alpha1": { 178 | "serverRelativeURL": "/openapi/v3/apis/hiveinternal.openshift.io/v1alpha1?hash=B7A5D74E3BDF4246D0C564E9B53B21B480E35D2919F2AEF1AECC553BF8E30210CCD326D59227447AC7FE2A9F0436C328A77B34C557E7CDDEF9C9AB53404FCBBF" 179 | }, 180 | "apis/image.openshift.io/v1": { 181 | "serverRelativeURL": "/openapi/v3/apis/image.openshift.io/v1?hash=392A0E121263F6A3D1F286A9F30532A7E3A2EFD37E252BAA39ED1E22CC97C10885AA2C0E17FA002584CE77CADA639618391661638F08B636F132968416EC2FB8" 182 | }, 183 | "apis/imageregistry.operator.openshift.io/v1": { 184 | "serverRelativeURL": "/openapi/v3/apis/imageregistry.operator.openshift.io/v1?hash=E3471D59C73FCAD4434DD49EC5E498C4C76FBF54D260F5A94EE31347842B2F20C27B76E886C51F1616A757E6C011F073BDF7C252E45BB146F077F21C7AFAC22A" 185 | }, 186 | "apis/infrastructure.cluster.x-k8s.io/v1alpha5": { 187 | "serverRelativeURL": "/openapi/v3/apis/infrastructure.cluster.x-k8s.io/v1alpha5?hash=84200E9C1BD7FD38DC6A75B45A1F008375CBD077C34C6F32CE18E496E3EBC3C543E2448A825F18C1D651268A91900438B5CA908210A8403E262C1466D056CAFA" 188 | }, 189 | "apis/infrastructure.cluster.x-k8s.io/v1beta1": { 190 | "serverRelativeURL": "/openapi/v3/apis/infrastructure.cluster.x-k8s.io/v1beta1?hash=2833B448783151A8FEFD7B887EBF080AC4A97BE6971DF9676F320DB0ABCF0B1EA1ABE9A7E4432EC305CED579E46EF2A23543545EC8FFA8849B47F4DF30DECD96" 191 | }, 192 | "apis/ingress.operator.openshift.io/v1": { 193 | "serverRelativeURL": "/openapi/v3/apis/ingress.operator.openshift.io/v1?hash=570E8A1FA23156A41A4EF65827FB0697399AB217BFB54BCF5596BB278A66A0E176AC4E68A38A199FEFE4855D59675761A72CE1E8E1675790EE1D1DA294A5611D" 194 | }, 195 | "apis/k8s.cni.cncf.io/v1": { 196 | "serverRelativeURL": "/openapi/v3/apis/k8s.cni.cncf.io/v1?hash=2BE373EB47E9DFF3F3F17B24F84F27D839145C3AC28BEF4D743D000E43147C0DEA986CB63F87BF833ED2EB85ADE1AA16693E4C49A16B2EB08D0B542678B3904F" 197 | }, 198 | "apis/kdmp.portworx.com/v1alpha1": { 199 | "serverRelativeURL": "/openapi/v3/apis/kdmp.portworx.com/v1alpha1?hash=5C87BA0A595B568192EEB40957E83A1F83670CF4FB65E6D3F9B17DC147DA7CCFB98FCF35D0527774C5A281D6DA369DBE6EAE7A60822BE7A5EE6C1E669A246DF1" 200 | }, 201 | "apis/lighthouse.submariner.io/v2alpha1": { 202 | "serverRelativeURL": "/openapi/v3/apis/lighthouse.submariner.io/v2alpha1?hash=C564F7D3A09B371172E92EDE620BD014EC732FC1A5C672C09A5BEE3ED71BFED4DEC4A98297F287D913AA1F79AC694BE7B40CA8799AA0B8F1C72B34724C81707A" 203 | }, 204 | "apis/logging.openshift.io/v1": { 205 | "serverRelativeURL": "/openapi/v3/apis/logging.openshift.io/v1?hash=999A0B03F3DA1FB0816726BDBB2ED06A64CD6048317981F973D31AC1D97C1F7FC7D3ACB0C611C35EBCF448957929BAE7AEDC09F52BE9E056E6532022E66B9A91" 206 | }, 207 | "apis/machine.openshift.io/v1": { 208 | "serverRelativeURL": "/openapi/v3/apis/machine.openshift.io/v1?hash=A1B310527BAFCF134F770C45BB3EAFDDE35C87111A75313B72D02DBAF4F5C4BEAC019729C5CC56FC5331217F6114699CAF9FEEB351F80CEDDF430E7241396A6B" 209 | }, 210 | "apis/machine.openshift.io/v1beta1": { 211 | "serverRelativeURL": "/openapi/v3/apis/machine.openshift.io/v1beta1?hash=58CBFB0B58AB37EE6E9C5F700A163E615702B259D6FE63D9F39474C4C9E5198F9910BA8EA8B8C3DF1D275514FB56981B55038B6D366B3980871E267C71A13092" 212 | }, 213 | "apis/machineconfiguration.openshift.io/v1": { 214 | "serverRelativeURL": "/openapi/v3/apis/machineconfiguration.openshift.io/v1?hash=ECA5BF536455AF5DFA3E560EB776E11BF65AA27D2310380B2C972E6DBF4A0A39EDCF62503376D76C9E5B57F4B8A3F005148C2C11A166924B07FF0E4B26536828" 215 | }, 216 | "apis/metal3.io/v1alpha1": { 217 | "serverRelativeURL": "/openapi/v3/apis/metal3.io/v1alpha1?hash=71182C6254D7999BFC5AB082001FD3B09C2117FB704CCC73E715E9607CBAD417140655593D4C62B9D5FFC764061C02BCDE0FC83AB5569F03252A9BD1C86423D2" 218 | }, 219 | "apis/metrics.k8s.io/v1beta1": { 220 | "serverRelativeURL": "/openapi/v3/apis/metrics.k8s.io/v1beta1?hash=72DECFB2B60E871CA0C437225A5FDB081DD0C917494B3A329E42CC8AFDC8E565EA4B3BFBCAC741032B9A07A01E2B4BB5276FC556CAEA3698D43E1E695D111851" 221 | }, 222 | "apis/migration.k8s.io/v1alpha1": { 223 | "serverRelativeURL": "/openapi/v3/apis/migration.k8s.io/v1alpha1?hash=820C67450CA14608ADDC69D864093AC27E091EE1CF02D48A2AB8D46F8A9E7040AFE00B752DC5213215A770F8BA8B0B66871DF8DD0BF481379312C3646BAC0F88" 224 | }, 225 | "apis/monitoring.coreos.com/v1": { 226 | "serverRelativeURL": "/openapi/v3/apis/monitoring.coreos.com/v1?hash=E6EFE3161D2AFAC39E16501F0053977F75B7796C02ADAD31CB73094054D6BA5476074744C1BE7A888A0139F711B7CF6718096E9882CC1E7387B831401634CD65" 227 | }, 228 | "apis/monitoring.coreos.com/v1alpha1": { 229 | "serverRelativeURL": "/openapi/v3/apis/monitoring.coreos.com/v1alpha1?hash=3677EA0FFD3F840F8C812DADF5999B395E0DAD7101AAE6F331992D3CDE0DBDEAFB377EA7FD4A5E1E4CDF61F314905C31FA259737CFA0EFBB06222C6FBD62C1A3" 230 | }, 231 | "apis/monitoring.coreos.com/v1beta1": { 232 | "serverRelativeURL": "/openapi/v3/apis/monitoring.coreos.com/v1beta1?hash=EFDB35CD72DE93EFC993BCBBBCA3542710B744CF6EA6B17A12BECE0859F4798A809941E97B3A197BA6A6EA5BD3B6E83AF11E3D2703709CDCCC470E0943D91817" 233 | }, 234 | "apis/multicluster.x-k8s.io/v1alpha1": { 235 | "serverRelativeURL": "/openapi/v3/apis/multicluster.x-k8s.io/v1alpha1?hash=3C21BDC1577C09254BDC16A204C495E1E350C57DC0436FD8B0E1ED6D2F8663E1114EC03B538108EFF7A01731E8A64541B3497A44CEB5B171C770C42CC523F3B3" 236 | }, 237 | "apis/network.openshift.io/v1": { 238 | "serverRelativeURL": "/openapi/v3/apis/network.openshift.io/v1?hash=D31DB767A298EC65EA76E0255C0C4523508F055E086D9C3AB19EB17E5A0C81B237983FAAB50A686C49F61D140F343C90D7DE49DC15894B95897822AFFB567BDC" 239 | }, 240 | "apis/network.operator.openshift.io/v1": { 241 | "serverRelativeURL": "/openapi/v3/apis/network.operator.openshift.io/v1?hash=54FC441599E0D065241A5B26DDD00376F47CF5E829F8990C2A7512EA01F87196A71BAAEC85A12BB0AD631F5638CBE7623E92B44547A2486E40D98D49C57E3540" 242 | }, 243 | "apis/networking.k8s.io": { 244 | "serverRelativeURL": "/openapi/v3/apis/networking.k8s.io?hash=29C711544F60CD0AACFEE6F5CA4893994E1E2003FAB043652B411AD2404E3DB30348F8818A8192100DA09EEE587B20D95B54C4FF9E60A8312CBA496F4F0CCA47" 245 | }, 246 | "apis/networking.k8s.io/v1": { 247 | "serverRelativeURL": "/openapi/v3/apis/networking.k8s.io/v1?hash=D03168BC6A2C9E724B140C633B050F8FC2B6053E1EF116F5EA68BDCF53C804249B789559BCD372F4B9AC3321FA5994469FD3A90A84BCD28AACF1E4346CD980AE" 248 | }, 249 | "apis/node.k8s.io": { 250 | "serverRelativeURL": "/openapi/v3/apis/node.k8s.io?hash=8959E01C2FE059EED55B80252C133B4472C753AF1FE56AFFBD6A8F1F56564F290363B721FB602BB03CC03B3327E05CCF0EBB6AA1D69020B9CFF8D3658B668D52" 251 | }, 252 | "apis/node.k8s.io/v1": { 253 | "serverRelativeURL": "/openapi/v3/apis/node.k8s.io/v1?hash=EB09B2B8F83EBD9BD46F2B417E134D99D1D7FDC5C7FDC4E5EF68B70FDE1DCAB09F5B5662F732800BA00B58DB46DCCAA71A18F91AF4C5F248AEB4C71DDB29A112" 254 | }, 255 | "apis/oauth.openshift.io/v1": { 256 | "serverRelativeURL": "/openapi/v3/apis/oauth.openshift.io/v1?hash=5768E4B78B99940C62604020A44EEE9630153A968D2D31E93ED74EE32065C932D1FB0A4FC71F8F08FB057C837FD89A4650F5DEFA62FE18D02C8E182409FB80BA" 257 | }, 258 | "apis/observability.open-cluster-management.io/v1beta1": { 259 | "serverRelativeURL": "/openapi/v3/apis/observability.open-cluster-management.io/v1beta1?hash=A6DB35D4FD704E108A24013925A416BBAFD33EA89A28123D8DFA0FE5DEBFC75D180BFF912A7C00C632E475AEBDC75927B27D3CD9F146F56BBE1680DB209D371B" 260 | }, 261 | "apis/observability.open-cluster-management.io/v1beta2": { 262 | "serverRelativeURL": "/openapi/v3/apis/observability.open-cluster-management.io/v1beta2?hash=3788D71E7F0A23F8DD8ED78E2AF21E63CD82D23C5227B6F4E3E8658371FA468B239F8EAE9B401DBEA9EFC200C8DB35AE4C3FBFBB007EAC089F97FEA69138EF74" 263 | }, 264 | "apis/operator.open-cluster-management.io/v1": { 265 | "serverRelativeURL": "/openapi/v3/apis/operator.open-cluster-management.io/v1?hash=81A7C26FF3E06DDC2BE39F4F4890C896AE946E6B375251231C3187F6AF37DBEF058B72803F781CF4A5B1AAAEAFC2D21E54013DCE33B54941BC460880A11369C6" 266 | }, 267 | "apis/operator.openshift.io/v1": { 268 | "serverRelativeURL": "/openapi/v3/apis/operator.openshift.io/v1?hash=BEB6740F46AEC5FECCC97C2FB5775BBA64FC27D023F281D50330AFAECF79A30850D3F6DD9FC79A3FDB0CC8E2EAD2081F758C9C0CB298A22E81345227710FB2DB" 269 | }, 270 | "apis/operator.openshift.io/v1alpha1": { 271 | "serverRelativeURL": "/openapi/v3/apis/operator.openshift.io/v1alpha1?hash=7947F5533AC3B5463BB852DDE93AC335AF1DC1E441F3B9E5CEB3E41711EBCCBAE98B6EBCA98A562A83609793D28070B55954E7BAFA7D7691DB395FE5934924C0" 272 | }, 273 | "apis/operators.coreos.com/v1": { 274 | "serverRelativeURL": "/openapi/v3/apis/operators.coreos.com/v1?hash=17A64E62AADF45B6C1F0FC14608771F141EFC7684EB374516CE9DC3AC3803B471DF9826CCCA020A1A20E09A9089C050195CF6C502652038F65BE18427F427F07" 275 | }, 276 | "apis/operators.coreos.com/v1alpha1": { 277 | "serverRelativeURL": "/openapi/v3/apis/operators.coreos.com/v1alpha1?hash=7A31E239903BD4621EFC025A2D5D03AFE9B6B62BCCAA5E61B026730DB25129484CB244B093A3DD78B4ECAE7059971BD1940C95DE18CF1A75298483FD1924290E" 278 | }, 279 | "apis/operators.coreos.com/v1alpha2": { 280 | "serverRelativeURL": "/openapi/v3/apis/operators.coreos.com/v1alpha2?hash=89862582934FC4AA6BB047FFBF7F5C12140306867874CEA4AE92DE0EBB7DE8930580A8A1D61791CD9C231EDF3D4043E94CB99E70E94A4FF4D2EBE369719F0471" 281 | }, 282 | "apis/operators.coreos.com/v2": { 283 | "serverRelativeURL": "/openapi/v3/apis/operators.coreos.com/v2?hash=46D3D65B00EC38F049D3DD1B90E71A7C16359B75D0EB4F3D625EC25AE99B999ED981FF5A6380A9108D3D0459BE27358FD8AFF538D8E6F88AE15A0C18B86228FE" 284 | }, 285 | "apis/packages.operators.coreos.com/v1": { 286 | "serverRelativeURL": "/openapi/v3/apis/packages.operators.coreos.com/v1?hash=5E6713CCA2E364BBF3132C076F96A7596F9592D82778194DE5001884B3C5D2859AA5E808DD6D93BF633061D1DB440EB1D9424E8F50D827381D00D6ED50AB99FA" 287 | }, 288 | "apis/performance.openshift.io/v1": { 289 | "serverRelativeURL": "/openapi/v3/apis/performance.openshift.io/v1?hash=EA9C2244B62EE766563473540CFB704813F057E2532728FB16CCBAE7563295BEBC378EDA11C3FDA56FBD00420C2C288D62EAC9147F4B61CF208F5FF3E4937EA8" 290 | }, 291 | "apis/performance.openshift.io/v1alpha1": { 292 | "serverRelativeURL": "/openapi/v3/apis/performance.openshift.io/v1alpha1?hash=717729C4D731284A3EC94AF613DD3E86E151CADE1DB43C673C48574E3DE98464A520EA4C45DC1E13B60BD5B8D18EF9E75B92D665689A46463C70C6AC8DFD4894" 293 | }, 294 | "apis/performance.openshift.io/v2": { 295 | "serverRelativeURL": "/openapi/v3/apis/performance.openshift.io/v2?hash=552D34A4AE4EB5FCDF3D768B7441D780E4DEEDD7C471DEB57166D6DC40220CF561296E42DBC5379693FC82F558C6CCA1EA7F2C24829D29F1365B0D4D1C4E3114" 296 | }, 297 | "apis/policy": { 298 | "serverRelativeURL": "/openapi/v3/apis/policy?hash=67B459A20E5812AE042A78968C9FFCFA33BB7BB5EF5265342BE295BE029D43CD1CD632C4A925E1C7DAD3DB2252EB83DEC381F0DDD2CFD762A6B6085CB77D1490" 299 | }, 300 | "apis/policy/v1": { 301 | "serverRelativeURL": "/openapi/v3/apis/policy/v1?hash=C8062D0B4FCFA93D55D7E26BA7B64A33D961948D73BE73C3446EFE0EFDDC435D06BA8D5B2EF5CA64ADED287096E887D2DBD2F61F0E2B3ACE6FF3CE299D19B955" 302 | }, 303 | "apis/portworx.io/v1beta2": { 304 | "serverRelativeURL": "/openapi/v3/apis/portworx.io/v1beta2?hash=900CA838394964348A03EDEE8A6807F534C411B5D54B5780855FF486809E15ECED8C8F51CC257B8263E1CF016532CBC66BE27FB8ECF7D346B830B909475367BC" 305 | }, 306 | "apis/project.openshift.io/v1": { 307 | "serverRelativeURL": "/openapi/v3/apis/project.openshift.io/v1?hash=392A0E121263F6A3D1F286A9F30532A7E3A2EFD37E252BAA39ED1E22CC97C10885AA2C0E17FA002584CE77CADA639618391661638F08B636F132968416EC2FB8" 308 | }, 309 | "apis/quay.redhat.com/v1": { 310 | "serverRelativeURL": "/openapi/v3/apis/quay.redhat.com/v1?hash=7D54B4D64F041FCEB3B118A3A21DC8F9F414AF0BE5CE4479ACEA744BCD38DA2822F3B10BD633E47B60AC0B5176E78B2AF9D2267E6107C8F3093975DC36FD85A8" 311 | }, 312 | "apis/quota.openshift.io/v1": { 313 | "serverRelativeURL": "/openapi/v3/apis/quota.openshift.io/v1?hash=92304EBC0130A8F918AF4FA77C9AA6CDFE14142A1738F4F97AC82E465543A54BB9B1EE6A59FC5A49503AD5363B9F040EC2FEE0E837927820991354A63C6D6E6C" 314 | }, 315 | "apis/rbac.authorization.k8s.io": { 316 | "serverRelativeURL": "/openapi/v3/apis/rbac.authorization.k8s.io?hash=62E429B99EBF7CA4CE190608F7A530374148736A81291321E781BEE3C19FEADCC0D87620ED3E930287628DB63E4CBFBE15E9EE25C8D008FBD35D30880EAAF4C5" 317 | }, 318 | "apis/rbac.authorization.k8s.io/v1": { 319 | "serverRelativeURL": "/openapi/v3/apis/rbac.authorization.k8s.io/v1?hash=1CEA3A0C082532D6876878D43DC5427B7E9FBF79C374D308A599BBB483178C6D130F6F955EEADBCF485C7EBF56607264BDEDD824D88989A73510BEEE42B8D70A" 320 | }, 321 | "apis/redhatcop.redhat.io/v1alpha1": { 322 | "serverRelativeURL": "/openapi/v3/apis/redhatcop.redhat.io/v1alpha1?hash=9E580C6CB534655CE153159B586078EC389C58ABA3A41493AE28DC61444FAD45039DDB0D0A19ED824DF8F6B2D0F3926BE5B9513C8863F738D447BD859B7E0BE7" 323 | }, 324 | "apis/route.openshift.io/v1": { 325 | "serverRelativeURL": "/openapi/v3/apis/route.openshift.io/v1?hash=392A0E121263F6A3D1F286A9F30532A7E3A2EFD37E252BAA39ED1E22CC97C10885AA2C0E17FA002584CE77CADA639618391661638F08B636F132968416EC2FB8" 326 | }, 327 | "apis/samples.operator.openshift.io/v1": { 328 | "serverRelativeURL": "/openapi/v3/apis/samples.operator.openshift.io/v1?hash=370D87CCC2486B54BF5B39C70DA9727074E7B241C5D1BE38D8B8340E6EA88A7F7C1ED5BFB6BDA51AA05F2855E441A67202CF3470C38334BA8BEC188674EA4D10" 329 | }, 330 | "apis/scheduling.k8s.io": { 331 | "serverRelativeURL": "/openapi/v3/apis/scheduling.k8s.io?hash=5DF941D3556633A5B51A680B443892D6C993914C53DD8DEA8EEB7C6C3A5F3B3F2578C8A4186FE844B7CBF4D92B352CE3D3439F414E29A9BFC9D11D5F657A57DE" 332 | }, 333 | "apis/scheduling.k8s.io/v1": { 334 | "serverRelativeURL": "/openapi/v3/apis/scheduling.k8s.io/v1?hash=816ED14DADBE0A9EFAF82475D0DEDA5F7496E7425473872733962B0AB21DA1D8EFC7AFB17C14B28C1014DCE05367FF3411CCBE0E1A81EF61EE9F9FD0D7FCA373" 335 | }, 336 | "apis/security.internal.openshift.io/v1": { 337 | "serverRelativeURL": "/openapi/v3/apis/security.internal.openshift.io/v1?hash=531F0FFF0AFD09CCABFBF43D19312B5418729960AAB148C5A3D215978EA30B31EAF2FAB9021A41A8F5BA20AE60B51340434BEF19AD54EB02C453FE622385DCCC" 338 | }, 339 | "apis/security.openshift.io/v1": { 340 | "serverRelativeURL": "/openapi/v3/apis/security.openshift.io/v1?hash=7F56836041533424E7C23466D0A7BEB8D3F2A5CC3B6AB7C39D958C518B0E3142AF1963667EC51463AD683662D66A88B20A57EBDA34423015CDA88F1B11244AED" 341 | }, 342 | "apis/snapshot.storage.k8s.io/v1": { 343 | "serverRelativeURL": "/openapi/v3/apis/snapshot.storage.k8s.io/v1?hash=DFA7FD3CA208222064871CF3A73E755AF74A605ABBA9AF6A931929E52EE941BA2796723892576F9D4D31892B6D4C0A265372DBE64E7059343C141FE8BC9F904D" 344 | }, 345 | "apis/storage.k8s.io": { 346 | "serverRelativeURL": "/openapi/v3/apis/storage.k8s.io?hash=432F9C444CD775833077BC1CD0831F235E3469E3C1227743EC29D6FAFBFB88AA13A2FEC471D4788B167261F978283DEC851BEB329407447BB80E242C0A5054E4" 347 | }, 348 | "apis/storage.k8s.io/v1": { 349 | "serverRelativeURL": "/openapi/v3/apis/storage.k8s.io/v1?hash=175E6735A634C1AC4914046BDDFA7D7F694862F0E941C4B9C5FD96769F96E2ADC4E1FA5AA37DCA04FD99FE52289766CE88741EA6088BBED8D119B59DF2C508AD" 350 | }, 351 | "apis/storage.k8s.io/v1beta1": { 352 | "serverRelativeURL": "/openapi/v3/apis/storage.k8s.io/v1beta1?hash=2C2AE221B9ADF29A5AC478CFA80305D60D4E7EFB0B4B0A16AB3A9F2528500893762A3645E476889B4052FE38B1331C26EB985EAB068AC9EC631027AF49E19019" 353 | }, 354 | "apis/stork.libopenstorage.org/v1alpha1": { 355 | "serverRelativeURL": "/openapi/v3/apis/stork.libopenstorage.org/v1alpha1?hash=E1B85F75713A2E5D8501943CB42C117ABCC37667DFF0A0893E834E28D3450D9DA64B782400EBF6D6D69A2CE6A2DC7B7066F88B11B09F5A0DD935BE97BA328045" 356 | }, 357 | "apis/submariner.io/v1": { 358 | "serverRelativeURL": "/openapi/v3/apis/submariner.io/v1?hash=57F99A0294C84D59683A21C401778E131700B110EB4160B81B230CFD72D7B20429532293FD6A4027BCF132CB6084650055A1E83CB9C378FD0D6B4F37031E5A59" 359 | }, 360 | "apis/submariner.io/v1alpha1": { 361 | "serverRelativeURL": "/openapi/v3/apis/submariner.io/v1alpha1?hash=A85FEA685B31695FC873046E773FE33EA2D41F000032BD42DF11D535661D6D8A98DF32D241F928D3007F2DF9503F589512779F333538C32344B967BEFAFE0CCC" 362 | }, 363 | "apis/submarineraddon.open-cluster-management.io/v1alpha1": { 364 | "serverRelativeURL": "/openapi/v3/apis/submarineraddon.open-cluster-management.io/v1alpha1?hash=5E4574D53785EECA0CEBBE1DB8D5000476E3A434E53FFBFFF98F383BE88E9F89EBA0368C333E7FB04E4F20AAEE9082D26732B4E404DDC3FF1F552F135F197B35" 365 | }, 366 | "apis/template.openshift.io/v1": { 367 | "serverRelativeURL": "/openapi/v3/apis/template.openshift.io/v1?hash=392A0E121263F6A3D1F286A9F30532A7E3A2EFD37E252BAA39ED1E22CC97C10885AA2C0E17FA002584CE77CADA639618391661638F08B636F132968416EC2FB8" 368 | }, 369 | "apis/tower.ansible.com/v1alpha1": { 370 | "serverRelativeURL": "/openapi/v3/apis/tower.ansible.com/v1alpha1?hash=9BAECA42A649825692C5479BEA8571234BBB1032AD37AD9963071BADBD406C0BBC28DAD8BF35B635984E010DF8D13230CB3E7E7032EEE11A24E08A60981ED32A" 371 | }, 372 | "apis/tuned.openshift.io/v1": { 373 | "serverRelativeURL": "/openapi/v3/apis/tuned.openshift.io/v1?hash=55F727F52C2413A82437B7EF5C01C8B7D1BA1147863D0B3E7C8460C47B1F22B5AF32DD61EBC8A00E10ECFA6338B0F004E9F2C01BDEB0E0E49166BB6C109979F0" 374 | }, 375 | "apis/user.openshift.io/v1": { 376 | "serverRelativeURL": "/openapi/v3/apis/user.openshift.io/v1?hash=5768E4B78B99940C62604020A44EEE9630153A968D2D31E93ED74EE32065C932D1FB0A4FC71F8F08FB057C837FD89A4650F5DEFA62FE18D02C8E182409FB80BA" 377 | }, 378 | "apis/volumesnapshot.external-storage.k8s.io/v1": { 379 | "serverRelativeURL": "/openapi/v3/apis/volumesnapshot.external-storage.k8s.io/v1?hash=F1BA0C5661D63C2CDF87695ADCD944AF942ECAB342A4295C0DDCC559BC0EE76159281B6E3028627FCFD39665BFCCBF58DB4E6C1F40311C269FF7AE01A2ECEA30" 380 | }, 381 | "apis/whereabouts.cni.cncf.io/v1alpha1": { 382 | "serverRelativeURL": "/openapi/v3/apis/whereabouts.cni.cncf.io/v1alpha1?hash=3A431142FD2A7B28772B451DBE936C6C5C90192206991C438EA54B0A5217FAB4EF3FB69134520B08F6ACD85293EBF84CD607D9E2F3F02AACDADA35801A5B0231" 383 | }, 384 | "openid/v1/jwks": { 385 | "serverRelativeURL": "/openapi/v3/openid/v1/jwks?hash=678C68C1B4A8EDD2810A49618A6541F200179476F13CEBA88C55C4BB2BAFAE8DF2D1205EE4B41DF434D785526B510C10AE27A373675F2A88D1B89C080C73D255" 386 | }, 387 | "version": { 388 | "serverRelativeURL": "/openapi/v3/version?hash=67F116BC50A6663F4BDDA75486B627865C26E67EE7BE8CD6D7BE22F9DFCA106785C4F367EED97F607538BA6EACACFA93A32789FBBDB783CF9D91E46F356C7932" 389 | } 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "k8s", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "./start.sh" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@wesleytodd/openapi": "^1.0.0", 14 | "docker-cli-js": "^2.10.0", 15 | "dotenv": "^16.4.5", 16 | "express": "^4.19.1", 17 | "istextorbinary": "^9.5.0", 18 | "mongoose": "^8.2.3", 19 | "node-cleanup": "^2.1.2", 20 | "portfinder": "^1.0.32", 21 | "uuid": "^9.0.1", 22 | "yaml": "^2.4.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pod_spec.txt: -------------------------------------------------------------------------------- 1 | spec 2 | configSource 3 | configMap 4 | kubeletConfigKey -required- 5 | name -required- 6 | namespace -required- 7 | resourceVersion 8 | uid 9 | externalID 10 | podCIDR 11 | podCIDRs <[]string> 12 | providerID 13 | taints <[]Taint> 14 | effect -required- 15 | key -required- 16 | timeAdded 17 | value 18 | unschedulable 19 | status 20 | addresses <[]NodeAddress> 21 | address -required- 22 | type -required- 23 | allocatable 24 | capacity 25 | conditions <[]NodeCondition> 26 | lastHeartbeatTime 27 | lastTransitionTime 28 | message 29 | reason 30 | status -required- 31 | type -required- 32 | config 33 | active 34 | configMap 35 | kubeletConfigKey -required- 36 | name -required- 37 | namespace -required- 38 | resourceVersion 39 | uid 40 | assigned 41 | configMap 42 | kubeletConfigKey -required- 43 | name -required- 44 | namespace -required- 45 | resourceVersion 46 | uid 47 | error 48 | lastKnownGood 49 | configMap 50 | kubeletConfigKey -required- 51 | name -required- 52 | namespace -required- 53 | resourceVersion 54 | uid 55 | daemonEndpoints 56 | kubeletEndpoint 57 | Port -required- 58 | images <[]ContainerImage> 59 | names <[]string> 60 | sizeBytes 61 | nodeInfo 62 | architecture -required- 63 | bootID -required- 64 | containerRuntimeVersion -required- 65 | kernelVersion -required- 66 | kubeProxyVersion -required- 67 | kubeletVersion -required- 68 | machineID -required- 69 | operatingSystem -required- 70 | osImage -required- 71 | systemUUID -required- 72 | phase 73 | volumesAttached <[]AttachedVolume> 74 | devicePath -required- 75 | name -required- 76 | volumesInUse <[]string> 77 | -------------------------------------------------------------------------------- /routes/certificateSigningRequest.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { CertificateSigningRequest } = require('../objects'); 3 | const { general, openapi } = require('../middleware'); 4 | 5 | const { apiCertificatesK8sIoApiV3, apiV1OpenapiV3, validSchema } = openapi; 6 | 7 | let routes = ['/apis/certificates.k8s.io/v1/certificatesigningrequests']; 8 | 9 | router.get(routes.map((e) => `${e}/:name`), validSchema(apiCertificatesK8sIoApiV3), general.findOne(CertificateSigningRequest)); 10 | 11 | router.get(['/api/v1/certificatesigningrequests', ...routes], validSchema(apiV1OpenapiV3), general.list(CertificateSigningRequest)); 12 | 13 | router.post(routes, validSchema(apiCertificatesK8sIoApiV3), general.save(CertificateSigningRequest)); 14 | 15 | router.put(routes, validSchema(apiCertificatesK8sIoApiV3), general.update(CertificateSigningRequest)); 16 | 17 | router.patch(routes.map((e) => `${e}/:name`), validSchema(apiCertificatesK8sIoApiV3), general.patch(CertificateSigningRequest)); 18 | 19 | router.delete(routes.map((e) => `${e}/:name`), validSchema(apiCertificatesK8sIoApiV3), general.deleteOne(CertificateSigningRequest)); 20 | 21 | router.delete(routes, validSchema(apiCertificatesK8sIoApiV3), general.delete(CertificateSigningRequest)); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /routes/clusterRole.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { ClusterRole } = require('../objects'); 3 | const { general, openapi } = require('../middleware'); 4 | 5 | const { apiRbacAuthorizatonK8sIoV1OpenApiV3, apiV1OpenapiV3, validSchema } = openapi; 6 | 7 | let routes = ['/apis/rbac.authorization.k8s.io/v1/clusterroles']; 8 | 9 | router.get(routes.map((e) => `${e}/:name`), validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.findOne(ClusterRole)); 10 | 11 | router.get(['/api/v1/deployments', ...routes], validSchema(apiV1OpenapiV3), general.list(ClusterRole)); 12 | 13 | router.post(routes, validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.save(ClusterRole)); 14 | 15 | router.put(routes, validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.update(ClusterRole)); 16 | 17 | router.patch(routes.map((e) => `${e}/:name`), validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.patch(ClusterRole)); 18 | 19 | router.delete(routes.map((e) => `${e}/:name`), validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.deleteOne(ClusterRole)); 20 | 21 | router.delete(routes, validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.delete(ClusterRole)); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /routes/clusterRoleBinding.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { ClusterRoleBinding } = require('../objects'); 3 | const { general, openapi } = require('../middleware'); 4 | 5 | const { apiRbacAuthorizatonK8sIoV1OpenApiV3, apiV1OpenapiV3, validSchema } = openapi; 6 | 7 | let routes = ['/apis/rbac.authorization.k8s.io/v1/clusterrolebindings']; 8 | 9 | router.get(routes.map((e) => `${e}/:name`), validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.findOne(ClusterRoleBinding)); 10 | 11 | router.get(['/api/v1/deployments', ...routes], validSchema(apiV1OpenapiV3), general.list(ClusterRoleBinding)); 12 | 13 | router.post(routes, validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.save(ClusterRoleBinding)); 14 | 15 | router.put(routes, validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.update(ClusterRoleBinding)); 16 | 17 | router.patch(routes.map((e) => `${e}/:name`), validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.patch(ClusterRoleBinding)); 18 | 19 | router.delete(routes.map((e) => `${e}/:name`), validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.deleteOne(ClusterRoleBinding)); 20 | 21 | router.delete(routes, validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.delete(ClusterRoleBinding)); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /routes/configMap.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { ConfigMap } = require('../objects'); 3 | const { general, openapi } = require('../middleware'); 4 | 5 | const { apiAppsV1OpenApiV3, apiV1OpenapiV3, validSchema } = openapi; 6 | 7 | let routes = ['/api/v1/namespaces/:namespace/configmaps']; 8 | 9 | router.get(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.findOne(ConfigMap)); 10 | 11 | router.get(['/api/v1/configmaps', ...routes], validSchema(apiV1OpenapiV3), general.list(ConfigMap)); 12 | 13 | router.post(routes, validSchema(apiAppsV1OpenApiV3), general.save(ConfigMap)); 14 | 15 | router.put(routes, validSchema(apiAppsV1OpenApiV3), general.update(ConfigMap)); 16 | 17 | router.patch(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.patch(ConfigMap)); 18 | 19 | router.delete(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.deleteOne(ConfigMap)); 20 | 21 | router.delete(routes, validSchema(apiAppsV1OpenApiV3), general.delete(ConfigMap)); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /routes/deployment.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { Deployment } = require('../objects'); 3 | const { general, openapi } = require('../middleware'); 4 | 5 | const { apiAppsV1OpenApiV3, apiV1OpenapiV3, validSchema } = openapi; 6 | 7 | let routes = ['/apis/apps/v1/namespaces/:namespace/deployments', '/api/v1/namespaces/:namespace/deployments']; 8 | 9 | router.get(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.findOne(Deployment)); 10 | 11 | router.get(['/api/v1/deployments', ...routes], validSchema(apiV1OpenapiV3), general.list(Deployment)); 12 | 13 | router.post(routes, validSchema(apiAppsV1OpenApiV3), general.save(Deployment)); 14 | 15 | router.put(routes, validSchema(apiAppsV1OpenApiV3), general.update(Deployment)); 16 | 17 | router.patch(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.patch(Deployment)); 18 | 19 | router.delete(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.deleteOne(Deployment)); 20 | 21 | router.delete(routes, validSchema(apiAppsV1OpenApiV3), general.delete(Deployment)); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /routes/endpoints.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { Endpoints } = require('../objects'); 3 | const { general, openapi } = require('../middleware'); 4 | 5 | const { apiAppsV1OpenApiV3, apiV1OpenapiV3, validSchema } = openapi; 6 | 7 | const routes = ['/apis/networking.k8s.io/v1/namespaces/:namespace/endpointses', '/api/v1/namespaces/:namespace/endpointses']; 8 | 9 | router.get(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.findOne(Endpoints)); 10 | 11 | router.get(['/api/v1/endpointses', ...routes], validSchema(apiV1OpenapiV3), general.list(Endpoints)); 12 | 13 | router.post(routes, validSchema(apiAppsV1OpenApiV3), general.save(Endpoints)); 14 | 15 | router.put(routes, validSchema(apiAppsV1OpenApiV3), general.update(Endpoints)); 16 | 17 | router.patch(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.patch(Endpoints)); 18 | 19 | router.delete(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.deleteOne(Endpoints)); 20 | 21 | router.delete(routes, validSchema(apiAppsV1OpenApiV3), general.delete(Endpoints)); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | api: require('./api.js'), 3 | deployment: require('./deployment.js'), 4 | ingress: require('./ingress.js'), 5 | namespace: require('./namespace.js'), 6 | pod: require('./pod.js'), 7 | endpoints: require('./endpoints.js'), 8 | namespaceCheck: require('./namespaceCheck.js'), 9 | configMap: require('./configMap.js'), 10 | secret: require('./secret.js'), 11 | service: require('./service.js'), 12 | certificateSigningRequest: require('./certificateSigningRequest.js'), 13 | clusterRole: require('./clusterRole.js'), 14 | role: require('./role.js'), 15 | clusterRoleBinding: require('./clusterRoleBinding.js'), 16 | roleBinding: require('./roleBinding.js'), 17 | openapi: require('./openapi.js'), 18 | }; 19 | -------------------------------------------------------------------------------- /routes/ingress.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { Ingress } = require('../objects'); 3 | const { general, openapi } = require('../middleware'); 4 | 5 | const { apiNetworkingK8sIoV1OpenApiV3, apiV1OpenapiV3, validSchema } = openapi; 6 | 7 | const routes = ['/apis/networking.k8s.io/v1/namespaces/:namespace/ingresses', '/api/v1/namespaces/:namespace/ingresses']; 8 | 9 | router.get(routes.map((e) => `${e}/:name`), validSchema(apiNetworkingK8sIoV1OpenApiV3), general.findOne(Ingress)); 10 | 11 | router.get(['/api/v1/ingresses', ...routes], validSchema(apiV1OpenapiV3), general.list(Ingress)); 12 | 13 | router.post(routes, validSchema(apiNetworkingK8sIoV1OpenApiV3), general.save(Ingress)); 14 | 15 | router.put(routes, validSchema(apiNetworkingK8sIoV1OpenApiV3), general.update(Ingress)); 16 | 17 | router.patch(routes.map((e) => `${e}/:name`), validSchema(apiNetworkingK8sIoV1OpenApiV3), general.patch(Ingress)); 18 | 19 | router.delete(routes.map((e) => `${e}/:name`), validSchema(apiNetworkingK8sIoV1OpenApiV3), general.deleteOne(Ingress)); 20 | 21 | router.delete(routes, validSchema(apiNetworkingK8sIoV1OpenApiV3), general.delete(Ingress)); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /routes/namespace.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { Namespace } = require('../objects'); 3 | const { general, openapi } = require('../middleware'); 4 | 5 | const { apiAppsV1OpenApiV3, apiV1OpenapiV3, validSchema } = openapi; 6 | 7 | let routes = ['/api/v1/namespaces']; 8 | 9 | router.get(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.findOne(Namespace)); 10 | 11 | router.get(routes, validSchema(apiV1OpenapiV3), general.list(Namespace)); 12 | 13 | router.post(routes, validSchema(apiAppsV1OpenApiV3), general.save(Namespace)); 14 | 15 | router.put(routes, validSchema(apiAppsV1OpenApiV3), general.update(Namespace)); 16 | 17 | router.patch(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.patch(Namespace)); 18 | 19 | router.delete(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.deleteOne(Namespace)); 20 | 21 | router.delete(routes, validSchema(apiAppsV1OpenApiV3), general.delete(Namespace)); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /routes/namespaceCheck.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { Namespace } = require('../objects'); 3 | 4 | router.use('/api/v1/namespaces/:namespace', (req, res, next) => { 5 | Namespace.findOne({ 'metadata.name': req.params.namespace }) 6 | .then((mamespace) => { 7 | if (mamespace) { 8 | return next(); 9 | } 10 | res.status(403).send(Namespace.forbiddenStatus(req.params.namespace)); 11 | }) 12 | .catch(next); 13 | }); 14 | 15 | module.exports = router; 16 | -------------------------------------------------------------------------------- /routes/openapi.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { 3 | openapiv3, 4 | apiV1OpenapiV3, 5 | apiNetworkingK8sIoV1OpenApiV3, 6 | apiAppsV1OpenApiV3, 7 | } = require('../middleware/openapi.js'); 8 | 9 | router.get('/openapi/v3', (req, res) => res.json(openapiv3.document)); 10 | router.get('/openapi/v3/apis/apps/v1', (req, res) => res.json(apiAppsV1OpenApiV3.document)); 11 | router.get(['/openapi/v3/apis/networking.k8s.io/v1', '/apis/networking.k8s.io/v1'], (req, res) => res.json(apiNetworkingK8sIoV1OpenApiV3.document)); 12 | router.get('/openapi/v3/api/v1', (req, res) => res.json(apiV1OpenapiV3.document)); 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /routes/pod.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { Pod } = require('../objects'); 3 | const { general, openapi } = require('../middleware'); 4 | 5 | const { apiAppsV1OpenApiV3, apiV1OpenapiV3, validSchema } = openapi; 6 | 7 | let routes = ['/apis/apps/v1/namespaces/:namespace/pods', '/api/v1/namespaces/:namespace/pods']; 8 | 9 | router.get(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.findOne(Pod)); 10 | 11 | router.get(['/api/v1/pods', ...routes], validSchema(apiV1OpenapiV3), general.list(Pod)); 12 | 13 | router.post(routes, validSchema(apiAppsV1OpenApiV3), general.save(Pod)); 14 | 15 | router.put(routes, validSchema(apiAppsV1OpenApiV3), general.update(Pod)); 16 | 17 | router.patch(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.patch(Pod)); 18 | 19 | router.delete(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.deleteOne(Pod)); 20 | 21 | router.delete(routes, validSchema(apiAppsV1OpenApiV3), general.delete(Pod)); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /routes/role.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { Role } = require('../objects'); 3 | const { general, openapi } = require('../middleware'); 4 | 5 | const { apiRbacAuthorizatonK8sIoV1OpenApiV3, apiV1OpenapiV3, validSchema } = openapi; 6 | 7 | let routes = ['/apis/rbac.authorization.k8s.io/v1/namespaces/:namespace/roles']; 8 | 9 | router.get(routes.map((e) => `${e}/:name`), validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.findOne(Role)); 10 | 11 | router.get(['/api/v1/roles', ...routes], validSchema(apiV1OpenapiV3), general.list(Role)); 12 | 13 | router.post(routes, validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.save(Role)); 14 | 15 | router.put(routes, validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.update(Role)); 16 | 17 | router.patch(routes.map((e) => `${e}/:name`), validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.patch(Role)); 18 | 19 | router.delete(routes.map((e) => `${e}/:name`), validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.deleteOne(Role)); 20 | 21 | router.delete(routes, validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.delete(Role)); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /routes/roleBinding.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { RoleBinding } = require('../objects'); 3 | const { general, openapi } = require('../middleware'); 4 | 5 | const { apiRbacAuthorizatonK8sIoV1OpenApiV3, apiV1OpenapiV3, validSchema } = openapi; 6 | 7 | let routes = ['/apis/rbac.authorization.k8s.io/v1/namespaces/:namespace/rolebindings']; 8 | 9 | router.get(routes.map((e) => `${e}/:name`), validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.findOne(RoleBinding)); 10 | 11 | router.get(['/api/v1/rolebindings', ...routes], validSchema(apiV1OpenapiV3), general.list(RoleBinding)); 12 | 13 | router.post(routes, validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.save(RoleBinding)); 14 | 15 | router.put(routes, validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.update(RoleBinding)); 16 | 17 | router.patch(routes.map((e) => `${e}/:name`), validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.patch(RoleBinding)); 18 | 19 | router.delete(routes.map((e) => `${e}/:name`), validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.deleteOne(RoleBinding)); 20 | 21 | router.delete(routes, validSchema(apiRbacAuthorizatonK8sIoV1OpenApiV3), general.delete(RoleBinding)); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /routes/secret.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { Secret } = require('../objects'); 3 | const { general, openapi } = require('../middleware'); 4 | 5 | const { apiAppsV1OpenApiV3, apiV1OpenapiV3, validSchema } = openapi; 6 | 7 | let routes = ['/api/v1/namespaces/:namespace/secrets']; 8 | 9 | router.get(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.findOne(Secret)); 10 | 11 | router.get(['/api/v1/secrets', ...routes], validSchema(apiV1OpenapiV3), general.list(Secret)); 12 | 13 | router.post(routes, validSchema(apiAppsV1OpenApiV3), general.save(Secret)); 14 | 15 | router.put(routes, validSchema(apiAppsV1OpenApiV3), general.update(Secret)); 16 | 17 | router.patch(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.patch(Secret)); 18 | 19 | router.delete(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.deleteOne(Secret)); 20 | 21 | router.delete(routes, validSchema(apiAppsV1OpenApiV3), general.delete(Secret)); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /routes/service.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { Service } = require('../objects'); 3 | const { general, openapi } = require('../middleware'); 4 | 5 | const { apiAppsV1OpenApiV3, apiV1OpenapiV3, validSchema } = openapi; 6 | 7 | let routes = ['/apis/apps/v1/namespaces/:namespace/services', '/api/v1/namespaces/:namespace/services']; 8 | 9 | router.get(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.findOne(Service)); 10 | 11 | router.get(['/api/v1/services', ...routes], validSchema(apiV1OpenapiV3), general.list(Service)); 12 | 13 | router.post(routes, validSchema(apiAppsV1OpenApiV3), general.save(Service)); 14 | 15 | router.put(routes, validSchema(apiAppsV1OpenApiV3), general.update(Service)); 16 | 17 | router.patch(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.patch(Service)); 18 | 19 | router.delete(routes.map((e) => `${e}/:name`), validSchema(apiAppsV1OpenApiV3), general.deleteOne(Service)); 20 | 21 | router.delete(routes, validSchema(apiAppsV1OpenApiV3), general.delete(Service)); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | docker rm -f loadbalancer 2 | docker image rm -f loadbalancer 3 | docker build -t loadbalancer -f loadBalancer/Dockerfile . 4 | docker image rm -f dns 5 | docker rm -f dns_server 6 | docker build -t dns -f dns/Dockerfile . 7 | docker run --expose 5333 --expose 8443 --expose 5334 --name dns_server -itd dns 8 | if [[ -z "${DB_URL}" ]]; then 9 | node ./index.js -dnsServer $(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' dns_server):5333 -dbName $(docker run -p 27017:27017 -p 27018:27018 -p 27019:27019 -p 27020:27020 -d mongo) 10 | else 11 | node ./index.js -dnsServer $(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' dns_server):5333 12 | fi 13 | --------------------------------------------------------------------------------