1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-05 13:55:21 +02:00

feat(kubernetes): introduce kubernetes support (#3987)

* feat(kubernetes): fix duplicate published mode

* feat(kubernetes): group port mappings by applications

* feat(kubernetes): updated UX

* feat(kubernetes): updated UX

* feat(kubernetes): new applications list view

* fix(kubernetes): applications - expand ports on row click

* refactor(kubernetes): applications - replace old view with new

* fix(kubernetes): disable access management for default resource pool

* feat(kubernetes): app creation - limit stacks suggestion to selected resource pool

* feat(kubernetes): do not allow access management on system resource pools

* refactor(kubernetes): refactor services

* create view node detail

* compute node status

* compute resource reservations

* resource reservation progress bar

* create applications node datatable

* fix(kubernetes): fix invalid method name

* feat(kubernetes): minor UI changes

* feat(kubernetes): update application inspect UI

* feat(kubernetes): add the ability to copy load balancer IP

* fix(kubernetes): minor fixes on applications view

* feat(kubernetes): set usage level info on progress bars

* fix(kubernetes): fix an issue with duplicate pagination controls

* fix(kubernetes): fix an issue with unexpandable items

* refacto(kubernetes): clean status and resource computation

* fix(kubernetes): remove a bad line

* feat(kubernetes): update application detail view

* feat(kubernetes): change few things on view

* refacto(kubernetes): Corrections relative to PR #13

* refacto(kubernetes): remove old functions

* feat(kubernetes): add application pod logs

* fix(kubernetes): PR #13

* feat(kubernetes): Enable quotas by default

* feat(kubernetes): allow non admin to have access to ressource pool list/detail view

* feat(kubernetes): UI changes

* fix(kubernetes): fix resource reservation computation in node view

* fix(kubernetes): pods are correctly filter by app name

* fix(kubernetes): nodeapplicationsdatatable is correctly reorder by cpu and memory

* fix(kubernetes): nodeapplications datatable is correctly reorder on reload

* feat(kubernetes): update podService

* refacto(kubernetes): rename nodeInspect as node

* refaceto(kubernetes): use colspan 6 instead of colspan 3

* refacto(kubernetes): use genericdatatablecontroller and make isadmin a binding

* refacto(kubernetes): remove not needed lines

* refacto(kubernetes) extract usageLevelInfo as html filter

* refacto(kubernetes): no line break for params

* refacto(kubernetes): change on node converter and filters

* refacto(kubernetes): remove bad indentations

* feat(kubernetes): add plain text informations about resources limits for non admibn user

* refacto(kubernetes): ES6 format

* refacto(kubernetes): format

* refacto(kubernetes): format

* refacto(kubernetes): add refresh callback for nodeapplicationsdatatable

* refacto(kubernetes): change if else structure

* refactor(kubernetes): files naming and format

* fix(kubernetes): remove checkbox and actions on resourcespools view for non admin

* feat(kubernetes): minor UI update

* fix(kubernetes): bind this on getPodsApplications to allow it to access $async

* fix(kubernetes): bind this on getEvents to allow it to access $async

* fix(kubernetes): format

* feat(kubernetes): minor UI update

* feat(kubernetes): add support for container console

* fix(kubernetes): fix a merge issue

* feat(kubernetes): update container console UI

* fix(api): fix typo

* feat(api): proxy pod websocket to agent

* fix(api): fix websocket pod proxy

* refactor(kubernetes): uniformize k8s merge comments

* refactor(kubernetes): update consoleController

* feat(kubernetes): prevent the removal of the default resource pool (#38)

* feat(kubernetes): show all applications running inside the resource pool (#35)

* add new datatable

* feat(kubernetes): add resource pool applications datatable to resource pool detail view

* refacto(kubernetes): factorise computeResourceReservation

* fix(kubernetes): colspan 6 to colspan 5

* fix(kubernetes): rename resourceReservationHelper into kubernetesResourceReservationHelper

* fix(kubernetes): add await to avoid double diggest cycles

* feat(kubernetes): add link to application name

* fix(kubernetes): change kubernetes-resource-pool-applications-datatable table key

* fix(kubernetes): change wording

* feat(kubernetes): add proper support for persisted folders (#36)

* feat(kubernetes): persistent volume mockups

* feat(kubernetes): persistent volume mockups

* feat(kubernetes): update persisted folders mockups

* feat(kubernetes): endpoint configure storage access policies

* fix(kubernetes): restrict advanced deployment to admin

* refactor(kubernetes): storageclass service / rest / model

* refactor(kubernetes): params/payload/converter pattern for deployments and daemonsets

* feat(kubernetes): statefulset management for applications

* fix(kubernets): associate application and pods

* feat(kubernetes): statefulset support for applications

* refactor(kubernetes): rebase on pportainer/k8s

* fix(kubernetes): app create - invalid targetPort on loadbalancer

* fix(kubernetes): internal services showed as loadbalancer

* fix(kubernetes): service ports creation / parsing

* fix(kubernetes): remove ports on headless services + ensure nodePort is used only for Cluster publishing

* fix(kubernetes): delete headless service on statefulset delete

* fix(kubernetes): statefulset replicas count display

* refactor(kubernetes): rebase on pportainer/k8s

* refactor(kubernetes): cleanup

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>

* fix(kubernetes): remove mockup routes

* feat(kubernetes): only display applications running on node/in resource pool when there are any

* feat(kubernetes): review resource reservations and leverage requests instead of limits (#40)

* fix(kubernetes): filter resource reservation by app in node view (#48)

* refactor(kubernetes): remove review comment

* chore(version): bump version number

* refactor(kubernetes): remove unused stacks view and components

* feat(kubernetes): update CPU slider step to 0.1 for resource pools (#60)

* feat(kubernetes): round up application CPU values (#61)

* feat(kubernetes): add information about application resource reservat… (#62)

* feat(kubernetes): add information about application resource reservations

* feat(kubernetes): apply kubernetesApplicationCPUValue to application CPU reservation

* refactor(kubernetes): services layer with models/converter/payloads (#64)

* refactor(kubernetes): services layer with models/converter/payloads

* refactor(kubernetes): file rename and comment update

* style(kubernetes): replace strings double quotes with simple quotes

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>

* fix(kubernetes): filter application by node in node detail view (#69)

* fix(kubernetes): filter applications by node

* fix(kubernetes): remove js error

* refactor(kubernetes): delete resource quota deletion process when deleting a resource pool (#68)

* feat(kubernetes): enforce valid resource reservations and clarify its… (#70)

* feat(kubernetes): enforce valid resource reservations and clarify its usage

* feat(kubernetes): update instance count input behavior

* feat(kubernetes): resource pools labels (#71)

* feat(kubernetes): resource pools labels

* fix(kubernetes): RP/RQ/LR owner label

* feat(kubernetes): confirmation popup on RP delete (#76)

* feat(kubernetes): application labels (#72)

* feat(kubernetes): application labels

* feat(kubernetes): display application owner in details when available

* style(kubernetes): revert StackName column labels

* fix(kubernetes): default displayed StackName

* feat(kubernetes): remove RQ query across cluster (#73)

* refactor(kubernetes): routes as components (#75)

* refactor(kubernetes): routes as components

* refactor(kubernetes): use component  lifecycle hook

* refactor(kubernetes): files naming consistency

* fix(kubernetes): fix invalid component name for cluster view

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>

* feat(kubernetes): update portaineruser cluster role policy rules (#78)

* refactor(kubernetes): remove unused helper

* fix(kubernetes): fix invalid reload link in cluster view

* feat(kubernetes): add cluster resource reservation (#77)

* feat(kubernetes): add cluster resource reservation

* fix(kubernetes): filter resource reservation with applications

* fix(kubernetes): fix indent

* refacto(kubernetes): extract megabytes value calc as resourceReservationHelper method

* fix(kubernetes): remove unused import

* refacto(kubernetes): add resourcereservation model

* fix(kubernetes): add parenthesis on arrow functions parameters

* refacto(kubernetes): getpods in applicationService getAll

* fix(kubernetes): let to const

* fix(kubernetes): remove unused podservice

* fix(kubernetes): fix computeResourceReservation

* fix(kubernetes): app.pods to app.Pods everywhere and camelcase of this.ResourceReservation

* feat(kubernetes): configurations list view (#74)

* feat(kubernetes): add configuration list view

* feat(kubernetes): add configurations datatable

* feat(kubernetes): add item selection

* feat(kubernetes): allow to remove configuration

* feat(kubernetes): allow non admin user to see configurations

* fix(kubernetes): configurations view as component

* feat(kubernetes): remove stack property for secret and configurations

* fix(kubernetes): update import

* fix(kubernetes): remove secret delete payload

* fix(kubernetes): rename configuration model

* fix(kubernetes): remove configmap delete payload

* fix(Kubernetes): fix configuration getAsync

* fix(kubernetes): extract params as variables

* refacto(kubernetes): extract configurations used lines as helper

* fix(kubernetes): add verification of _.find return value

* fix(kubernetes): fix kubernetes configurations datatable callback

* refacto(Kubernetes): extract find before if

* fix(kubernetes): replace this by KubernetesConfigurationHelper in static method

* fix(Kubernetes): fix getASync

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>

* review(kubernetes): todo comments (#80)

* feat(kubernetes): minor UI update

* feat(kubernetes): round max cpu value in application creation

* feat(kubernetes): minor UI update

* fix(kubernetes): no-wrap resource reservation bar text (#89)

* docs(kubernetes): add review for formValues to resource conversion (#91)

* feat(kubernetes): configuration creation view (#82)

* feat(kubernetes): create configuration view

* feat(kubernetes): add advanced mode and create entry from file

* fix(kubernetes): fix validation issues

* fix(kubernetes): fix wording

* fix(kubernetes): replace data by stringdata in secret payloads

* fix(kubernetes): rename KubernetesConfigurationEntry to KubernetesConfigurationFormValuesDataEntry

* refacto(kubernetes): add isSimple to formValues and change configuration creation pattern

* fix(kubernetes): fix some bugs

* refacto(kubernetes): renaming

* fix(kubernetes): fix few bugs

* fix(kubernetes): fix few bugs

* review(kubernetes): refactor notices

Co-authored-by: xAt0mZ <baron_l@epitech.eu>

* feat(kubernetes): rename codeclimate file

* feat(kubernetes): re-enable codeclimate

* feat(project): update codeclimate configuration

* feat(project): update codeclimate configuration

* feat(project): update codeclimate configuration

* feat(kubernetes): minor UI update

* feat(project): update codeclimate

* feat(project): update codeclimate configuration

* feat(project): update codeclimate configuration

* feat(kubernetes): configuration details view (#93)

* feat(kubernetes): configuration details view

* fix(kubernetes): fix wording

* fix(kubernetes): fix update button

* fix(kubernetes): line indent

* refacto(kubernetes): remove conversion

* refacto(kubernetes): remove useless line

* refacto(kubernetes): remove useless lines

* fix(kubernetes): revert error handling

* fix(kubernetes): fix wording

* fix(kubernetes): revert line deletion

* refacto(kubernetes): change data mapping

* fix(kubernetes): create before delete

* fix(kubernetes): fix duplicate bug

* feat(kubernetes): configurations in application creation (#92)

* feat(kubernetes): application configuration mockups

* feat(kubernetes): update mockup

* feat(kubernetes): app create - dynamic view for configurations

* feat(kubernetes): app create - configuration support

* refactor(kubernetes): more generic configuration conversion function

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>

* feat(kubernetes): automatically display first entry in configuration creation

* feat(kubernetes): minor UI update regarding applications and configurations

* feat(kubernetes): update Cluster icon in sidebar

* feat(kubernetes): volumes list view (#112)

* feat(kubernetes): add a feedback panel on main views (#111)

* feat(kubernetes): add a feedback panel on main views

* feat(kubernetes): add feedback panel to volumes view

* fix(kubernetes): isolated volumes showed as unused even when used (#116)

* feat(kubernetes): remove limit range from Portainer (#119)

* limits instead of requests (#121)

* feat(kubernetes): volume details (#117)

* feat(kubernetes): volume details

* fix(kubernetes): yaml not showed

* feat(kubernetes): expandable stacks list (#122)

* feat(kubernetes): expandable stacks list

* feat(kubernetes): minor UI update to stacks datatable

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>

* feat(kubernetes): uibprogress font color (#129)

* feat(kubernetes): minor UI update to resource reservation component

* feat(kubernetes): automatically select a configuration

* refactor(kubernetes): remove comment

* feat(kubernetes): minor UI update

* feat(kubernetes): add resource links and uniformize view headers (#133)

* feat(kubernetes): prevent removal of system configurations (#128)

* feat(kubernetes): prevent removal of system configurations

* fix(kubernetes): KubernetesNamespaceHelper is not a function

* refacto(kubernetes): change prevent removal pattern

* fix(kubernetes): remove unused dependencies

* fix(kubernetes): fix configuration used label (#123)

* fix(kubernetes): fix used configurations

* fix(kubernetes): remove console log

* feat(kubernetes): rename configuration types (#127)

* refacto(kubernetes): fix wording and use configMap instead of Basic in the code

* feat(kubernetes): prevent the removal of system configuration

* fix(kubernetes): remove feat on bad branch

* fix(kubernetes): rename configuration types

* refacto(kubernetes): use a numeric enum and add a filter to display the text type

* refacto(kubernetes): fix wording and use configMap instead of Basic in the code

* feat(kubernetes): prevent the removal of system configuration

* fix(kubernetes): remove feat on bad branch

* fix(kubernetes): rename configuration types

* refacto(kubernetes): use a numeric enum and add a filter to display the text type

* fix(kubernetes): rename file and not use default in switch case

* feat(kubernetes): update advanced deployment UI/UX (#130)

* feat(kubernetes): update advanced deployment UI/UX

* feat(kubernetes): review HTML tags indentation

* feat(kubernetes): applications stacks delete (#135)

* fix(kubernetes): multinode resources reservations (#118)

* fix(kubernetes): filter pods by node

* fix(kubernetes): fix applications by node filter

* fix(kubernetes): filter pods by node

* Update app/kubernetes/views/cluster/node/nodeController.js

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

* feat(kubernetes): limit usage of pod console view (#136)

* feat(kubernetes): add yaml and events to configuration details (#126)

* feat(kubernetes): add yaml and events to configuration details

* fix(kubernetes): fix errors on secret details view

* fix(kubernetes): display only events related to configuration

* fix(kubernetes): fix applications by node filter

* fix(kubernetes): revert commit on bad branch

* refacto(kubernetes): refacto configmap get yaml function

* refacto(kubernetes): add yaml into converter

* feat(kubernetes): improve application details (#141)

* refactor(kubernetes): remove applications retrieval from volume service

* feat(kubernetes): improve application details view

* feat(kubernetes): update kompose binary version (#143)

* feat(kubernetes): update kubectl version (#144)

* refactor(kubernetes): rename portainer system namespace (#145)

* feat(kubernetes): add a loading view indicator (#140)

* feat(kubernetes): add an example of view loading indicator

* refactor(css): remove comment

* feat(kubernetes): updated loading pattern

* feat(kubernetes): add loading indicator for resource pool views

* feat(kubernetes): add loading indicator for deploy view

* feat(kubernetes): add loading view indicator to dashboard

* feat(kubernetes): add loading view indicator to configure view

* feat(kubernetes): add loading indicator to configuration views

* feat(kubernetes): add loading indicator to cluster views

* feat(kubernetes): rebase on k8s branch

* feat(kubernetes): update icon size

* refactor(kubernetes): update indentation and tag format

* feat(kubernetes): backend role validation for stack deployment (#147)

* feat(kubernetes): show applications when volume is used

* feat(kubernetes): set empty value when node is not set

* feat(kubernetes): update configuration UI/UX

* feat(kubernetes): update configuration UX

* fix(kubernetes): Invalid value for a configuration (#139)

* fix(kubernetes): Invalid value for a configuration

* fix(kubernetes): remove auto JSON convertion for configMap ; apply it for RPool Accesses only

* refactor(kubernetes): remove unneeded line

* fix(kubernetes): remove default JSON parsing on configMap API retrieval

Co-authored-by: xAt0mZ <baron_l@epitech.eu>

* feat(kubernetes): applications table in configuration details (#154)

* feat(kubernetes): Add the ability to filter system resources (#142)

* feat(kubernetes): hide system configurations

* feat(kubernetes): Add the ability to filter system resources

* feat(kubernetes): add the ability to hide system resources on volumes

* fix(kubernetes): fix few issue in volumesDatatableController

* fix(kubernetes): fix applications / ports / stacks labels

* feat(kubernetes): add volumes and configurations to dashboard (#152)

* feat(kubernetes): event warning indicator (#150)

* feat(kubernetes): event warning indicator for applications

* refactor(kubernetes): refactor events indicator logic

* feat(kubernetes): add event warning indicator to all resources

* feat(kubernetes): fix missing YAML panel for node (#157)

* feat(kubernetes): revised application details view (#159)

* feat(kubernetes): revised application details view

* refactor(kubernetes): remove comment

* feat(kubernetes): rebase on k8s

* refactor(kubernetes): remove extra line

* feat(kubernetes): update kubernetes beta feedback panel locations (#161)

* feat(kubernetes): stack logs (#160)

* feat(kubernetes): stack logs

* fix(kubernetes): ignore starting pods

* fix(kubernetes): colspan on expandable stack applications table

* feat(kubernetes): add an information message about system resources (#163)

* fix(kubernetes): fix empty panel being display in cluster view (#165)

* fix(kubernetes): Invalid CPU unit for node (#156)

* fix(kubernetes): Invalid CPU unit for node

* fix(kubernetes): Invalid CPU unit for node

* refacto(kubernetes): extract parseCPU function in helper

* refacto(kubernetes): rewrite parseCPU function

* feat(kubernetes): add the kube-node-lease namespace to system namespaces (#177)

* feat(kubernetes): tag system applications on node details view (#175)

* feat(kubernetes): tag system applications on node details view

* fix(kubernetes): remove system resources filter

* feat(kubernetes): review UI/UX around volume size unit (#178)

* feat(kubernetes): updates after review (#174)

* feat(kubernetes): update access user message

* feat(kubernetes): relocate resource pool to a specific form section

* feat(kubernetes): review responsiveness of port mappings

* feat(kubernetes): clarify table settings

* feat(kubernetes): add resource reservation summary message

* feat(kubernetes): review wording (#182)

* feat(kubernetes): application stack edit (#179)

* feat(kubernetes): update UI -- update action missing

* feat(kubernetes): application stack update

* feat(kubernetes): change services stacks

* feat(kubernetes): hide default-tokens + prevent remove (#183)

* feat(kubernetes): hide default-tokens + prevent remove

* feat(kubernetes): do not display unused label for system configurations

* fix(kubernetes): minor fix around showing system configurations

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>

* feat(kubernetes): rebase on k8s branch (#180)

* fix(kubernetes): prevent the display of system resources in dashboard (#186)

* fix(kubernetes): prevent the display of system resources in dashboard

* fix(kubernetes): prevent the display of frontend filtered resource pools

* feat(kubernetes): support downward API for env vars in application details (#181)

* feat(kubernetes): support downward API for env vars in application details

* refactor(kubernetes): remove comment

* feat(kubernetes): minor UI update

* feat(kubernetes): remove Docker features (#189)

* chore(version): bump version number (#187)

* chore(version): bump version number

* feat(kubernetes): disable update notice

* feat(kubernetes): minor UI update

* feat(kubernetes): minor UI update

* feat(kubernetes): form validation (#170)

* feat(kubernetes): add published node port value check

* feat(kubernetes): add a dns compliant validation

* fix(kubernetes): fix port range validation

* feat(kubernetes): lot of form validation

* feat(kubernetes): add lot of form validation

* feat(kubernetes): persisted folders size validation

* feat(kubernetes): persisted folder path should be unique

* fix(kubernetes): fix createResourcePool button

* fix(kubernetes): change few things

* fix(kubernetes): fix slider memory

* fix(kubernetes): fix duplicates on dynamic field list

* fix(kubernetes): remove bad validation on keys

* feat(kubernetes): minor UI enhancements and validation updates

* feat(kubernetes): minor UI update

* fix(kubernetes):  revert on slider fix

* review(kubernetes): add future changes to do

* fix(kubernetes): add form validation on create application memory slider

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
Co-authored-by: xAt0mZ <baron_l@epitech.eu>

* feat(kubernetes): remove Docker related content

* feat(kubernetes): update build system to remove docker binary install

* fix(kubernetes): fix an issue with missing user settings

* feat(kubernetes): created column for apps and resource pools (#184)

* feat(kubernetes): created column for apps and resource pools

* feat(kubernetes): configurations and volumes owner

* feat(kubernetes): rename datatables columns

* fix(kubernetes): auto detect statefulset headless service name (#196)

* fix(applications): display used configurations (#198)

* feat(kubernetes): app details - display data access policy (#199)

* feat(kubernetes): app details - display data access policy

* feat(kubernetes): tooltip on data access info

* feat(kubernetes): move DAP tooltip to end of line

* feat(kubernetes): minor UI update

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>

* fix(kubernetes): fix an issue when updating the local endpoint (#204)

* fix(kubernetes): add unique key to configuration overriden key path field (#207)

* feat(kubernetes): tag applications as external (#221)

* feat(kubernetes): tag applications as external first approach

* feat(kubernetes): tag applications as external

* feat(kubernetes): Use ibytes as the default volume size unit sent to the Kubernetes API (#222)

* feat(kubernetes): Use ibytes as the default volume size unit sent to the Kubernetes API

* fix(kubernetes): only display b units in list and details views

* feat(kubernetes): add note to application details (#212)

* feat(kubernetes): add note to application details

* fix(kubernetes): remove eslintcache

* feat(kubernetes): update application note UI

* feat(kubernetes): add an update button to the note form when a note is already associated to an app

* feat(kubernetes): fix with UI changes

* fix(kubernetes): change few things

* fix(kubernetes): remove duplicate button

* fix(kubernetes): just use a ternary

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>

* feat(kubernetes): fix data persistence display for isolated DAP (#223)

* feat(kubernetes): add a quick action to copy application name to clipboard (#225)

* feat(kubernetes): revert useless converter changes (#228)

* feat(kubernetes): edit application view (#200)

* feat(kubernetes): application to formValues conversion

* feat(kubernetes): extract applicationFormValues conversion as converter function

* feat(kubernetes): draft app patch

* feat(kubernetes): patch on all apps services + service service + pvc service

* feat(kubernetes): move name to labels and use UUID as kubernetes Name + patch recreate if necessary

* feat(kubernetes): move user app name to label and use UUID for Kubernetes Name field

* feat(kubernetes): kubernetes service patch mechanism

* feat(kubernetes): application edit

* feat(kubernetes): remove stack edit on app details

* feat(kubernetes): revert app name saving in label - now reuse kubernetes Name field

* feat(kubernetes): remove the ability to edit the DAP

* feat(kubernetes): cancel button on edit view

* feat(kubernetes): remove ability to add/remove persisted folders for SFS edition

* feat(kubernetes): minor UI update and action changes

* feat(kubernetes): minor UI update

* feat(kubernetes): remove ability to edit app volumes sizes + disable update button if no changes are made + codeclimate

* fix(kubernetes): resource reservation sliders in app edit

* fix(kubernetes): patch returned with 422 when trying to create nested objects

* fix(kubernetes): changing app deployment type wasn't working (delete failure)

* style(kubernetes): codeclimate

* fix(kubernetes): app edit - limits sliders max value

* feat(kubernetes): remove prefix on service name as we enforce DNS compliant app names

* fix(kubernetes): edit app formvalues replica based on target replica count and not total pods count

* fix(kubernetes): disable update for RWO on multi replica + delete service when changing app type

* fix(kubernetes): app details running / target pods display

* feat(kubernetes): add partial patch for app details view

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>

* feat(kubernetes): disable edit capability for external and system apps (#233)

* feat(kubernetes): minor UI update

* fix(kubernetes): edit application issues (#235)

* feat(kubernetes): disable edition of load balancer if it's in pending state

* fix(kubernetes): now able to change from LB to other publishing types

* feat(kuberntes): modal on edit click to inform on potential service interruption

* feat(kubernetes): hide note when empty + add capability to collapse it

* fix(kubernetes): UI/API desync + app update button enabled in some cases where it shouldn't be

* fix(kubernetes): all apps are now using rolling updates with specific conditions

* style(kubernetes): code indent

* fix(kubernetes): disable sync process on endpoint init as current endpoint is not saved in client state

* fix(kubernetes): sliders refresh on app create + app details bad display for sfs running pods

* feat(kubernetes): minor UI update

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>

* feat(kubernetes): bump up kubectl version to v1.18.0

* feat(kubernetes): when refreshing a view, remember currently opened tabs (#226)

* feat(kubernetes): When refreshing a view, remember currently opened tabs

* fix(kubernetes): only persist the current tab inside the actual view

* fix(kubernetes): not working with refresh in view header

* fix(kubernetes): skip error on 404 headless service retrieval if missconfigured in sfs (#242)

* refactor(kubernetes): use KubernetesResourcePoolService instead of KubernetesNamespaceService (#243)

* fix(kubernetes): create service before app to enforce port availability (#239)

* fix(kubernetes): external flag on application ports mappings datatable (#245)

* refactor(kubernetes): remove unused KubernetesResourcePoolHelper (#246)

* refactor(kubernetes): make all *service.getAllAsync functions consistent (#249)

* feat(kubernetes): Tag external applications in the application table of the resource pool details view (#251)

* feat(kubernetes): add ability to redeploy application (#240)

* feat(kubernetes): add ability to redeploy application

* feat(kubernetes): allow redeploy for external apps

* Revert "feat(kubernetes): allow redeploy for external apps"

This reverts commit 093375a7e93c1a07b845ebca1618da034a97fbcd.

* refactor(kubernetes): use KubernetesPodService instead of REST KubernetesPods (#247)

* feat(kubernetes): prevent configuration properties edition (#248)

* feat(kubernetes): prevent configuration properties edition

* feat(kubernetes): Relocate the Data/Actions to a separate panel

* feat(kubernetes): remove unused functions

* feat(kubernetes): minor UI update

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>

* refactor(kubernetes): Simplify the FileReader usage (#254)

* refactor(kubernetes): simplify FileReader usage

* refactor(kubernetes): Simplify FileReader usage

* refactor(kubernetes): rename e as event for readability

* feat(kubernetes): Tag system Configs in the Config details view (#257)

* refactor(kubernetes): Refactor the isFormValid function of multiple controllers (#253)

* refactor(kubernetes): refactor isFormValid functions in configurations

* refactor(kubernetes): refactor isformValid functions in create application

* refactor(kubernetes): remove duplicate lines

* refactor(kubernetes): remove commented line

* feat(kubernetes): Tag external volumes and configs (#250)

* feat(kubernetes): Tag external volumes and configs

* feat(kubernetes): remove .eslintcache

* feat(kubernetes): change few things

* feat(kubernetes): don't tag system configuration as external

* feat(kubernetes): minor UI update

* feat(kubernetes): extract inline css and clean all tags

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>

* fix(kubernetes): daemon set edit (#258)

* fix(kubernetes): persistent folder unit parsing

* fix(kubernetes): edit daemonset on RWO storage

* fix(kubernetes): external SFS had unlinked volumes (#264)

* feat(kubernetes): prevent to override two different configs on the same filesystem path (#259)

* feat(kubernetes): prevent to override two different configs on the same filesystem path

* feat(kubernetes): The validation should only be triggered across Configurations.

* feat(kubernetes): fix validations issues

* feat(kubernetes): fix form validation

* feat(kubernetes): fix few things

* refactor(kubernetes): Review the code mirror component update for configurations (#260)

* refactor(kubernetes): extract duplicate configuration code into a component

* refactor(kubernetes): fix form validation issues

* refactor(kubernetes): fix missing value

* refactor(kubernetes): remove useless await

* feat(kubernetes): Update the shared access policy configuration for Storage (#263)

* feat(kubernetes): Update the shared access policy configuration for Storage

* Update app/kubernetes/models/storage-class/models.js

* feat(kubernetes): remove ROX references and checks

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
Co-authored-by: xAt0mZ <baron_l@epitech.eu>

* feat(kubernetes): provide the remove/restore UX for environment variables when editing an application (#261)

* feat(kubernetes): Provide the remove/restore UX for environment variables when editing an application

* feat(kubernetes): fix ui issue

* feat(kubernetes): change few things

* fix(kubernetes): Invalid display for exposed ports in accessing the application section (#267)

* feat(kubernetes): application rollback (#269)

* feat(kubernetes): retrieve all versions of a deployment

* feat(kubernetes): application history for all types

* feat(kubernetes): deployment rollback

* feat(kubernetes): daemonset / statefulset rollback

* feat(kubernetes): remove the revision selector and rollback on previous version everytime

* feat(kubernetes): minor UI changes

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>

* feat(kubernetes): reservations should be computed based on requests instead of limits (#268)

* feat(kubernetes): Reservations should be computed based on requests instead of limits

* feat(kubernetes): use requests instead of limits in application details

* feat(kubernetes): removes unused limits

* feat(kubernetes): Not so useless

* feat(kubernetes): use service selectors to bind apps and services (#270)

* feat(kubernetes): use service selectors to bind apps and services

* Update app/kubernetes/services/statefulSetService.js

* style(kubernetes): remove comment block

Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

* chore(version): bump version number

* feat(kubernetes): update feedback panel text

* chore(app): add prettier to k8s

* style(app): apply prettier to k8s codebase

* fix(kubernetes): Cannot read property 'port' of undefined (#272)

* fix(kubernetes): Cannot read property 'port' of undefined

* fix(kubernetes): concat app ports outside publishedports loop

* fix(application): fix broken display of the persistence layer (#274)

* chore(kubernetes): fix conflicts

* chore(kubernetes): fix issues related to conflict resolution

* refactor(kubernetes): refactor code related to conflict resolution

* fix(kubernetes): fix a minor issue with assets import

* chore(app): update yarn.lock

* fix(application): ports mapping are now correctly detected (#300)

* fix(build-system): fix missing docker binary download step

* feat(kubernetes): application auto scaling details (#301)

* feat(kubernetes): application auto scaling details

* feat(kubernetes): minor UI update

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>

* feat(kubernetes): Introduce a "used by" column in the volume list view (#303)

Co-authored-by: xAt0mZ <baron_l@epitech.eu>
Co-authored-by: Maxime Bajeux <max.bajeux@gmail.com>
Co-authored-by: xAt0mZ <xAt0mZ@users.noreply.github.com>
This commit is contained in:
Anthony Lapenna 2020-07-06 11:21:03 +12:00 committed by GitHub
parent 24528ecea8
commit af6bea5acc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
361 changed files with 20794 additions and 2349 deletions

261
app/kubernetes/__module.js Normal file
View file

@ -0,0 +1,261 @@
angular.module('portainer.kubernetes', ['portainer.app']).config([
'$stateRegistryProvider',
function ($stateRegistryProvider) {
'use strict';
const kubernetes = {
name: 'kubernetes',
url: '/kubernetes',
parent: 'root',
abstract: true,
resolve: {
endpointID: [
'EndpointProvider',
'$state',
function (EndpointProvider, $state) {
const id = EndpointProvider.endpointID();
if (!id) {
return $state.go('portainer.home');
}
},
],
},
};
const applications = {
name: 'kubernetes.applications',
url: '/applications',
views: {
'content@': {
component: 'kubernetesApplicationsView',
},
},
};
const applicationCreation = {
name: 'kubernetes.applications.new',
url: '/new',
views: {
'content@': {
component: 'kubernetesCreateApplicationView',
},
},
};
const application = {
name: 'kubernetes.applications.application',
url: '/:namespace/:name',
views: {
'content@': {
component: 'kubernetesApplicationView',
},
},
};
const applicationEdit = {
name: 'kubernetes.applications.application.edit',
url: '/edit',
views: {
'content@': {
component: 'kubernetesCreateApplicationView',
},
},
};
const applicationConsole = {
name: 'kubernetes.applications.application.console',
url: '/:pod/console',
views: {
'content@': {
component: 'kubernetesApplicationConsoleView',
},
},
};
const applicationLogs = {
name: 'kubernetes.applications.application.logs',
url: '/:pod/logs',
views: {
'content@': {
component: 'kubernetesApplicationLogsView',
},
},
};
const stacks = {
name: 'kubernetes.stacks',
url: '/stacks',
abstract: true,
};
const stack = {
name: 'kubernetes.stacks.stack',
url: '/:namespace/:name',
abstract: true,
};
const stackLogs = {
name: 'kubernetes.stacks.stack.logs',
url: '/logs',
views: {
'content@': {
component: 'kubernetesStackLogsView',
},
},
};
const configurations = {
name: 'kubernetes.configurations',
url: '/configurations',
views: {
'content@': {
component: 'kubernetesConfigurationsView',
},
},
};
const configurationCreation = {
name: 'kubernetes.configurations.new',
url: '/new',
views: {
'content@': {
component: 'kubernetesCreateConfigurationView',
},
},
};
const configuration = {
name: 'kubernetes.configurations.configuration',
url: '/:namespace/:name',
views: {
'content@': {
component: 'kubernetesConfigurationView',
},
},
};
const cluster = {
name: 'kubernetes.cluster',
url: '/cluster',
views: {
'content@': {
component: 'kubernetesClusterView',
},
},
};
const node = {
name: 'kubernetes.cluster.node',
url: '/:name',
views: {
'content@': {
component: 'kubernetesNodeView',
},
},
};
const dashboard = {
name: 'kubernetes.dashboard',
url: '/dashboard',
views: {
'content@': {
component: 'kubernetesDashboardView',
},
},
};
const deploy = {
name: 'kubernetes.deploy',
url: '/deploy',
views: {
'content@': {
component: 'kubernetesDeployView',
},
},
};
const resourcePools = {
name: 'kubernetes.resourcePools',
url: '/pools',
views: {
'content@': {
component: 'kubernetesResourcePoolsView',
},
},
};
const resourcePoolCreation = {
name: 'kubernetes.resourcePools.new',
url: '/new',
views: {
'content@': {
component: 'kubernetesCreateResourcePoolView',
},
},
};
const resourcePool = {
name: 'kubernetes.resourcePools.resourcePool',
url: '/:id',
views: {
'content@': {
component: 'kubernetesResourcePoolView',
},
},
};
const resourcePoolAccess = {
name: 'kubernetes.resourcePools.resourcePool.access',
url: '/access',
views: {
'content@': {
component: 'kubernetesResourcePoolAccessView',
},
},
};
const volumes = {
name: 'kubernetes.volumes',
url: '/volumes',
views: {
'content@': {
component: 'kubernetesVolumesView',
},
},
};
const volume = {
name: 'kubernetes.volumes.volume',
url: '/:namespace/:name',
views: {
'content@': {
component: 'kubernetesVolumeView',
},
},
};
$stateRegistryProvider.register(kubernetes);
$stateRegistryProvider.register(applications);
$stateRegistryProvider.register(applicationCreation);
$stateRegistryProvider.register(application);
$stateRegistryProvider.register(applicationEdit);
$stateRegistryProvider.register(applicationConsole);
$stateRegistryProvider.register(applicationLogs);
$stateRegistryProvider.register(stacks);
$stateRegistryProvider.register(stack);
$stateRegistryProvider.register(stackLogs);
$stateRegistryProvider.register(configurations);
$stateRegistryProvider.register(configurationCreation);
$stateRegistryProvider.register(configuration);
$stateRegistryProvider.register(cluster);
$stateRegistryProvider.register(dashboard);
$stateRegistryProvider.register(deploy);
$stateRegistryProvider.register(node);
$stateRegistryProvider.register(resourcePools);
$stateRegistryProvider.register(resourcePoolCreation);
$stateRegistryProvider.register(resourcePool);
$stateRegistryProvider.register(resourcePoolAccess);
$stateRegistryProvider.register(volumes);
$stateRegistryProvider.register(volume);
},
]);

View file

@ -0,0 +1,157 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
<div ng-if="$ctrl.refreshCallback" class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Table settings</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader">
Table settings
</div>
<div class="menuContent">
<div>
<div class="md-checkbox">
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate">
Refresh rate
</label>
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
<option value="120">2min</option>
<option value="300">5min</option>
</select>
<span>
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
</span>
</div>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
</div>
</div>
</div>
</span>
</div>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Images')">
Image
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Images' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Images' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Status')">
Status
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Node')">
Node
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Node' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Node' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('CreationDate')">
Creation date
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CreationDate' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CreationDate' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit: $ctrl.tableKey))"
pagination-id="$ctrl.tableKey"
>
<td>{{ item.Name }}</td>
<td
><span ng-repeat="image in item.Images track by $index">{{ image }}<br /></span
></td>
<td
><span class="label label-{{ item.Status | kubernetesPodStatusColor }}">{{ item.Status }}</span></td
>
<td>
<span ng-if="item.Node">
<a ui-sref="kubernetes.cluster.node({ name: item.Node })">
{{ item.Node }}
</a>
</span>
<span ng-if="!item.Node">-</span>
</td>
<td>{{ item.CreationDate | getisodate }}</td>
<td>
<a ui-sref="kubernetes.applications.application.logs({ pod: item.Name })"> <i class="fa fa-file-alt" aria-hidden="true"></i> Logs </a>
<a ng-if="item.Status === 'Running'" ui-sref="kubernetes.applications.application.console({ pod: item.Name })" style="margin-left: 10px;">
<i class="fa fa-terminal" aria-hidden="true"></i> Console
</a>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="6" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="6" class="text-center text-muted">No pod available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5" pagination-id="$ctrl.tableKey"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,12 @@
angular.module('portainer.kubernetes').component('kubernetesPodsDatatable', {
templateUrl: './podsDatatable.html',
controller: 'GenericDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
refreshCallback: '<',
},
});

View file

@ -0,0 +1,193 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
<span class="small text-muted" style="float: left; margin-top: 5px;" ng-if="$ctrl.isAdmin && !$ctrl.settings.showSystem">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
System resources are hidden, this can be changed in the table settings.
</span>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Table settings</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader">
Table settings
</div>
<div class="menuContent">
<div>
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
<input id="applications_setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
<label for="applications_setting_show_system">Show system resources</label>
</div>
<div class="md-checkbox">
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate">
Refresh rate
</label>
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
<option value="120">2min</option>
<option value="300">5min</option>
</select>
<span>
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
</span>
</div>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
</div>
</div>
</div>
</span>
</div>
</div>
<div class="actionBar">
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.applications.new">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add application
</button>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('StackName')">
Stack
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'StackName' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'StackName' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('ResourcePool')">
Resource pool
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourcePool' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourcePool' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Image')">
Image
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Image' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Image' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
Deployment
</th>
<th>
Publishing mode
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('CreationDate')">
Created
<i class="fa fa-sort-numeric-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CreationDate' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-numeric-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CreationDate' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | filter:$ctrl.isDisplayed | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit: $ctrl.tableKey))"
ng-class="{ active: item.Checked }"
pagination-id="$ctrl.tableKey"
>
<td>
<span class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" ng-disabled="$ctrl.isSystemNamespace(item)" />
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool })">{{ item.Name }}</a>
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item)">system</span>
<span class="label label-primary image-tag label-margins" ng-if="!$ctrl.isSystemNamespace(item) && $ctrl.isExternalApplication(item)">external</span>
</td>
<td>{{ item.StackName }}</td>
<td>
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.ResourcePool })">{{ item.ResourcePool }}</a>
</td>
<td>{{ item.Image }}</td>
<td>
<span ng-if="item.DeploymentType === $ctrl.KubernetesApplicationDeploymentTypes.REPLICATED">Replicated</span>
<span ng-if="item.DeploymentType === $ctrl.KubernetesApplicationDeploymentTypes.GLOBAL">Global</span>
<code>{{ item.RunningPodsCount }}</code> / <code>{{ item.TotalPodsCount }}</code></td
>
<td>
<span ng-if="item.PublishedPorts.length">
<span>
<a ng-click="$ctrl.onPublishingModeClick(item)">
<i class="fa {{ item.ServiceType | kubernetesApplicationServiceTypeIcon }}" aria-hidden="true" style="margin-right: 2px;"> </i>
{{ item.ServiceType | kubernetesApplicationServiceTypeText }}
</a>
</span>
</span>
<span ng-if="item.PublishedPorts.length === 0">-</span>
</td>
<td>{{ item.CreationDate | getisodate }} {{ item.ApplicationOwner ? 'by ' + item.ApplicationOwner : '' }}</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="7" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="7" class="text-center text-muted">No application available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5" pagination-id="$ctrl.tableKey"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,15 @@
angular.module('portainer.kubernetes').component('kubernetesApplicationsDatatable', {
templateUrl: './applicationsDatatable.html',
controller: 'KubernetesApplicationsDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
removeAction: '<',
refreshCallback: '<',
onPublishingModeClick: '<',
},
});

View file

@ -0,0 +1,77 @@
import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
angular.module('portainer.docker').controller('KubernetesApplicationsDatatableController', [
'$scope',
'$controller',
'KubernetesNamespaceHelper',
'DatatableService',
'Authentication',
function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
var ctrl = this;
this.settings = Object.assign(this.settings, {
showSystem: false,
});
this.onSettingsShowSystemChange = function () {
DatatableService.setDataTableSettings(this.tableKey, this.settings);
};
this.isExternalApplication = function (item) {
return KubernetesApplicationHelper.isExternalApplication(item);
};
this.isSystemNamespace = function (item) {
return KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool);
};
this.isDisplayed = function (item) {
return !ctrl.isSystemNamespace(item) || ctrl.settings.showSystem;
};
/**
* Do not allow applications in system namespaces to be selected
*/
this.allowSelection = function (item) {
return !this.isSystemNamespace(item);
};
this.$onInit = function () {
this.isAdmin = Authentication.isAdmin();
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
this.setDefaults();
this.prepareTableFromDataset();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
if (textFilter !== null) {
this.state.textFilter = textFilter;
this.onTextFilterChange();
}
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
if (this.filters && this.filters.state) {
this.filters.state.open = false;
}
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
if (storedSettings !== null) {
this.settings = storedSettings;
this.settings.open = false;
}
this.onSettingsRepeaterChange();
};
},
]);

View file

@ -0,0 +1,192 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
<span class="small text-muted" style="float: left; margin-top: 5px;" ng-if="$ctrl.isAdmin && !$ctrl.settings.showSystem">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
System resources are hidden, this can be changed in the table settings.
</span>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Table settings</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader">
Table settings
</div>
<div class="menuContent">
<div>
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
<input id="ports_setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
<label for="ports_setting_show_system">Show system resources</label>
</div>
<div class="md-checkbox">
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate">
Refresh rate
</label>
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
<option value="120">2min</option>
<option value="300">5min</option>
</select>
<span>
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
</span>
</div>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
</div>
</div>
</div>
</span>
</div>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
<a ng-click="$ctrl.expandAll()" ng-if="$ctrl.hasExpandableItems()">
<i ng-class="{ 'fas fa-angle-down': $ctrl.state.expandAll, 'fas fa-angle-right': !$ctrl.state.expandAll }" aria-hidden="true"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Name')">
Application
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
Publishing mode
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('TargetPort')">
Exposed port
<i class="fa fa-sort-numeric-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'TargetPort' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-numeric-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'TargetPort' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Port')">
Container port
<i class="fa fa-sort-numeric-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Port' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-numeric-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Port' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Protocol')">
Protocol
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Protocol' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Protocol' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate-start="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | filter: $ctrl.isDisplayed | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit: $ctrl.tableKey))"
ng-class="{ active: item.Checked }"
ng-style="{ background: item.Highlighted ? '#d5e8f3' : '' }"
ng-click="$ctrl.expandItem(item, !item.Expanded)"
pagination-id="$ctrl.tableKey"
>
<td>
<a ng-if="$ctrl.itemCanExpand(item)">
<i ng-class="{ 'fas fa-angle-down': item.Expanded, 'fas fa-angle-right': !item.Expanded }" class="space-right" aria-hidden="true"></i>
</a>
</td>
<td>
<a ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool })">{{ item.Name }}</a>
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item)">system</span>
<span class="label label-primary image-tag label-margins" ng-if="!$ctrl.isSystemNamespace(item) && $ctrl.isExternalApplication(item)">external</span>
</td>
<td>
<span ng-if="item.ServiceType === 'LoadBalancer'">
<span> <i class="fa fa-project-diagram" aria-hidden="true" style="margin-right: 2px;"></i> Load balancer </span>
<span class="text-muted small" style="margin-left: 5px;">
<span ng-if="item.LoadBalancerIPAddress">{{ item.LoadBalancerIPAddress }}</span>
<span ng-if="!item.LoadBalancerIPAddress">pending</span>
</span>
</span>
<span ng-if="item.ServiceType === 'ClusterIP'"> <i class="fa fa-list-alt" aria-hidden="true" style="margin-right: 2px;"></i> Internal </span>
<span ng-if="item.ServiceType === 'NodePort'"> <i class="fa fa-list" aria-hidden="true" style="margin-right: 2px;"></i> Cluster </span>
</td>
<td ng-if="!$ctrl.itemCanExpand(item)">
{{ item.Ports[0].Port }}
<a ng-if="item.LoadBalancerIPAddress" ng-href="http://{{ item.LoadBalancerIPAddress }}:{{ item.Ports[0].Port }}" target="_blank" style="margin-left: 5px;">
<i class="fa fa-external-link-alt" aria-hidden="true"></i> access
</a>
</td>
<td ng-if="!$ctrl.itemCanExpand(item)">{{ item.Ports[0].TargetPort }}</td>
<td ng-if="!$ctrl.itemCanExpand(item)">{{ item.Ports[0].Protocol }}</td>
<td ng-if="$ctrl.itemCanExpand(item)"></td>
<td ng-if="$ctrl.itemCanExpand(item)"></td>
<td ng-if="$ctrl.itemCanExpand(item)"></td>
</tr>
<tr dir-paginate-end ng-show="item.Expanded" ng-repeat="port in item.Ports" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }">
<td></td>
<td>-</td>
<td>-</td>
<td>
{{ port.Port }}
<a ng-if="item.LoadBalancerIPAddress" ng-href="http://{{ item.LoadBalancerIPAddress }}:{{ port.Port }}" target="_blank" style="margin-left: 5px;">
<i class="fa fa-external-link-alt" aria-hidden="true"></i> access
</a>
</td>
<td>{{ port.TargetPort }}</td>
<td>{{ port.Protocol }}</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="6" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="6" class="text-center text-muted">No application port mapping available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5" pagination-id="$ctrl.tableKey"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,13 @@
angular.module('portainer.kubernetes').component('kubernetesApplicationsPortsDatatable', {
templateUrl: './applicationsPortsDatatable.html',
controller: 'KubernetesApplicationsPortsDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
refreshCallback: '<',
},
});

View file

@ -0,0 +1,103 @@
import _ from 'lodash-es';
import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
angular.module('portainer.docker').controller('KubernetesApplicationsPortsDatatableController', [
'$scope',
'$controller',
'KubernetesNamespaceHelper',
'DatatableService',
'Authentication',
function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
this.state = Object.assign(this.state, {
expandedItems: [],
expandAll: false,
});
var ctrl = this;
this.settings = Object.assign(this.settings, {
showSystem: false,
});
this.onSettingsShowSystemChange = function () {
DatatableService.setDataTableSettings(this.tableKey, this.settings);
};
this.isExternalApplication = function (item) {
return KubernetesApplicationHelper.isExternalApplication(item);
};
this.isSystemNamespace = function (item) {
return KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool);
};
this.isDisplayed = function (item) {
return !ctrl.isSystemNamespace(item) || ctrl.settings.showSystem;
};
this.expandItem = function (item, expanded) {
if (!this.itemCanExpand(item)) {
return;
}
item.Expanded = expanded;
if (!expanded) {
item.Highlighted = false;
}
};
this.itemCanExpand = function (item) {
return item.Ports.length > 1;
};
this.hasExpandableItems = function () {
return _.filter(this.state.filteredDataSet, (item) => this.itemCanExpand(item)).length;
};
this.expandAll = function () {
this.state.expandAll = !this.state.expandAll;
_.forEach(this.state.filteredDataSet, (item) => {
if (this.itemCanExpand(item)) {
this.expandItem(item, this.state.expandAll);
}
});
};
this.$onInit = function () {
this.isAdmin = Authentication.isAdmin();
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
this.setDefaults();
this.prepareTableFromDataset();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
if (textFilter !== null) {
this.state.textFilter = textFilter;
this.onTextFilterChange();
}
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
if (this.filters && this.filters.state) {
this.filters.state.open = false;
}
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
if (storedSettings !== null) {
this.settings = storedSettings;
this.settings.open = false;
}
this.onSettingsRepeaterChange();
};
},
]);

View file

@ -0,0 +1,183 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
<span class="small text-muted" style="float: left; margin-top: 5px;" ng-if="$ctrl.isAdmin && !$ctrl.settings.showSystem">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
System resources are hidden, this can be changed in the table settings.
</span>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Table settings</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader">
Table settings
</div>
<div class="menuContent">
<div>
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
<input id="stacks_setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
<label for="stacks_setting_show_system">Show system resources</label>
</div>
<div class="md-checkbox">
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate">
Refresh rate
</label>
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
<option value="120">2min</option>
<option value="300">5min</option>
</select>
<span>
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
</span>
</div>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
</div>
</div>
</div>
</span>
</div>
</div>
<div class="actionBar">
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.expandAll()" ng-if="$ctrl.hasExpandableItems()">
<i ng-class="{ 'fas fa-angle-down': $ctrl.state.expandAll, 'fas fa-angle-right': !$ctrl.state.expandAll }" aria-hidden="true"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Name')">
Stack
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('ResourcePool')">
Resource pool
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourcePool' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourcePool' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Applications.length')">
Applications
<i class="fa fa-sort-numeric-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Applications.length' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-numeric-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Applications.length' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
Actions
</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate-start="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | filter: $ctrl.isDisplayed | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit: $ctrl.tableKey))"
ng-class="{ active: item.Checked }"
ng-style="{ background: item.Highlighted ? '#d5e8f3' : '' }"
ng-click="$ctrl.expandItem(item, !item.Expanded)"
pagination-id="$ctrl.tableKey"
>
<td>
<span class="md-checkbox">
<input
id="select_{{ $index }}"
type="checkbox"
ng-model="item.Checked"
ng-click="$ctrl.selectItem(item, $event); $event.stopPropagation();"
ng-disabled="!$ctrl.allowSelection(item)"
/>
<label for="select_{{ $index }}"></label>
</span>
<a ng-if="$ctrl.itemCanExpand(item)">
<i ng-class="{ 'fas fa-angle-down': item.Expanded, 'fas fa-angle-right': !item.Expanded }" class="space-right" aria-hidden="true"></i>
</a>
</td>
<td>
{{ item.Name }}
</td>
<td>
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.ResourcePool })" ng-click="$event.stopPropagation();">{{ item.ResourcePool }}</a>
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item)">system</span>
</td>
<td>{{ item.Applications.length }}</td>
<td>
<a ui-sref="kubernetes.stacks.stack.logs({ namespace: item.ResourcePool, name: item.Name })"> <i class="fa fa-file-alt" aria-hidden="true"></i> Logs </a>
</td>
</tr>
<tr dir-paginate-end ng-show="item.Expanded" ng-repeat="app in item.Applications" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }">
<td colspan="5">
<a ui-sref="kubernetes.applications.application({ name: app.Name, namespace: app.ResourcePool })" style="margin-left: 25px;">{{ app.Name }}</a>
<span style="margin-left: 5px;" class="label label-primary image-tag" ng-if="!$ctrl.isSystemNamespace(app.ResourcePool) && $ctrl.isExternalApplication(app)"
>external</span
>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="5" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="5" class="text-center text-muted">No stack available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5" pagination-id="$ctrl.tableKey"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,14 @@
angular.module('portainer.kubernetes').component('kubernetesApplicationsStacksDatatable', {
templateUrl: './applicationsStacksDatatable.html',
controller: 'KubernetesApplicationsStacksDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
refreshCallback: '<',
removeAction: '<',
},
});

View file

@ -0,0 +1,110 @@
import _ from 'lodash-es';
import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
angular.module('portainer.docker').controller('KubernetesApplicationsStacksDatatableController', [
'$scope',
'$controller',
'KubernetesNamespaceHelper',
'DatatableService',
'Authentication',
function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
this.state = Object.assign(this.state, {
expandedItems: [],
expandAll: false,
});
var ctrl = this;
this.settings = Object.assign(this.settings, {
showSystem: false,
});
this.onSettingsRepeaterChange = function () {
DatatableService.setDataTableSettings(this.tableKey, this.settings);
};
this.isExternalApplication = function (item) {
return KubernetesApplicationHelper.isExternalApplication(item);
};
/**
* Do not allow applications in system namespaces to be selected
*/
this.allowSelection = function (item) {
return !this.isSystemNamespace(item);
};
this.isSystemNamespace = function (item) {
return KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool);
};
this.isDisplayed = function (item) {
return !ctrl.isSystemNamespace(item) || ctrl.settings.showSystem;
};
this.expandItem = function (item, expanded) {
if (!this.itemCanExpand(item)) {
return;
}
item.Expanded = expanded;
if (!expanded) {
item.Highlighted = false;
}
};
this.itemCanExpand = function (item) {
return item.Applications.length > 0;
};
this.hasExpandableItems = function () {
return _.filter(this.state.filteredDataSet, (item) => this.itemCanExpand(item)).length;
};
this.expandAll = function () {
this.state.expandAll = !this.state.expandAll;
_.forEach(this.state.filteredDataSet, (item) => {
if (this.itemCanExpand(item)) {
this.expandItem(item, this.state.expandAll);
}
});
};
this.$onInit = function () {
this.isAdmin = Authentication.isAdmin();
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
this.setDefaults();
this.prepareTableFromDataset();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
if (textFilter !== null) {
this.state.textFilter = textFilter;
this.onTextFilterChange();
}
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
if (this.filters && this.filters.state) {
this.filters.state.open = false;
}
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
if (storedSettings !== null) {
this.settings = storedSettings;
this.settings.open = false;
}
this.onSettingsRepeaterChange();
};
},
]);

View file

@ -0,0 +1,162 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
<span class="small text-muted" style="float: left; margin-top: 5px;" ng-if="$ctrl.isAdmin && !$ctrl.settings.showSystem">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
System resources are hidden, this can be changed in the table settings.
</span>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Table settings</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader">
Table settings
</div>
<div class="menuContent">
<div>
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
<input id="setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
<label for="setting_show_system">Show system resources</label>
</div>
<div class="md-checkbox">
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate">
Refresh rate
</label>
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
<option value="120">2min</option>
<option value="300">5min</option>
</select>
<span>
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
</span>
</div>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
</div>
</div>
</div>
</span>
</div>
</div>
<div class="actionBar">
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.configurations.new">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add configuration
</button>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Namespace')">
Resource Pool
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Namespace' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Namespace' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Type')">
Type
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Type' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Type' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('CreationDate')">
Created
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CreationDate' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CreationDate' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | filter:$ctrl.isDisplayed | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
>
<td>
<span class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" ng-disabled="!$ctrl.allowSelection(item)" />
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="kubernetes.configurations.configuration({ name: item.Name, namespace: item.Namespace })">{{ item.Name }}</a>
<span class="label label-primary image-tag label-margins" ng-if="!$ctrl.isSystemConfig(item) && $ctrl.isExternalConfiguration(item)">external</span>
<span class="label label-warning image-tag label-margins" ng-if="!item.Used && !$ctrl.isSystemNamespace(item) && !$ctrl.isSystemConfig(item)">unused</span>
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemConfig(item)">system</span>
</td>
<td>
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.Namespace })">{{ item.Namespace }}</a>
</td>
<td>{{ item.Type | kubernetesConfigurationTypeText }}</td>
<td>{{ item.CreationDate | getisodate }} {{ item.ConfigurationOwner ? 'by ' + item.ConfigurationOwner : '' }}</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="4" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="4" class="text-center text-muted">No configuration available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,13 @@
angular.module('portainer.kubernetes').component('kubernetesConfigurationsDatatable', {
templateUrl: './configurationsDatatable.html',
controller: 'KubernetesConfigurationsDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
refreshCallback: '<',
removeAction: '<',
},
});

View file

@ -0,0 +1,83 @@
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
angular.module('portainer.docker').controller('KubernetesConfigurationsDatatableController', [
'$scope',
'$controller',
'KubernetesNamespaceHelper',
'DatatableService',
'Authentication',
function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
const ctrl = this;
this.settings = Object.assign(this.settings, {
showSystem: false,
});
this.onSettingsShowSystemChange = function () {
DatatableService.setDataTableSettings(this.tableKey, this.settings);
};
this.isSystemNamespace = function (item) {
return KubernetesNamespaceHelper.isSystemNamespace(item.Namespace);
};
this.isSystemToken = function (item) {
return KubernetesConfigurationHelper.isSystemToken(item);
};
this.isSystemConfig = function (item) {
return ctrl.isSystemNamespace(item) || ctrl.isSystemToken(item);
};
this.isExternalConfiguration = function (item) {
return KubernetesConfigurationHelper.isExternalConfiguration(item);
};
this.isDisplayed = function (item) {
return !ctrl.isSystemConfig(item) || (ctrl.settings.showSystem && ctrl.isAdmin);
};
/**
* Do not allow configurations in system namespaces to be selected
*/
this.allowSelection = function (item) {
return !this.isSystemConfig(item) && !item.Used;
};
this.$onInit = function () {
this.isAdmin = Authentication.isAdmin();
this.setDefaults();
this.prepareTableFromDataset();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
if (textFilter !== null) {
this.state.textFilter = textFilter;
this.onTextFilterChange();
}
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
if (this.filters && this.filters.state) {
this.filters.state.open = false;
}
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
if (storedSettings !== null) {
this.settings = storedSettings;
this.settings.open = false;
}
this.onSettingsRepeaterChange();
};
},
]);

View file

@ -0,0 +1,134 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Table settings</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader">
Table settings
</div>
<div class="menuContent">
<div>
<div class="md-checkbox">
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate">
Refresh rate
</label>
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
<option value="120">2min</option>
<option value="300">5min</option>
</select>
<span>
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
</span>
</div>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
</div>
</div>
</div>
</span>
</div>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
<a ng-click="$ctrl.changeOrderBy('Date')">
Date
<i class="fa fa-sort-numeric-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Date' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-numeric-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Date' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Involved.kind')">
Kind
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Involved.kind' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Involved.kind' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Type')">
Type
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Type' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Type' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Message')">
Message
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Message' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Message' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit: $ctrl.tableKey))"
pagination-id="$ctrl.tableKey"
>
<td>{{ item.Date | getisodate }}</td>
<td>{{ item.Involved.kind }}</td>
<td
><span class="label label-{{ item.Type | kubernetesEventTypeColor }}">{{ item.Type }}</span></td
>
<td>{{ item.Message }}</td>
</tr>
<tr ng-if="$ctrl.loading">
<td colspan="4" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="!$ctrl.loading && (!$ctrl.dataset || $ctrl.state.filteredDataSet.length === 0)">
<td colspan="4" class="text-center text-muted">No event available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5" pagination-id="$ctrl.tableKey"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,14 @@
angular.module('portainer.kubernetes').component('kubernetesEventsDatatable', {
templateUrl: './eventsDatatable.html',
controller: 'GenericDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
loading: '<',
refreshCallback: '<',
},
});

View file

@ -0,0 +1,127 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i>
{{ $ctrl.titleText }}
</div>
<div ng-if="$ctrl.refreshCallback" class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle> <i class="fa fa-cog" aria-hidden="true"></i> Table settings </span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader">
Table settings
</div>
<div class="menuContent">
<div>
<div class="md-checkbox">
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate">
Refresh rate
</label>
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
<option value="120">2min</option>
<option value="300">5min</option>
</select>
<span>
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
</span>
</div>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
</div>
</div>
</div>
</span>
</div>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('StackName')">
Stack
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'StackName' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'StackName' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Image')">
Image
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Image' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Image' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
>
<td
><a ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool })">{{ item.Name }}</a></td
>
<td>{{ item.StackName }}</td>
<td>{{ item.Image }}</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="3" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="3" class="text-center text-muted">No application available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,13 @@
angular.module('portainer.kubernetes').component('kubernetesIntegratedApplicationsDatatable', {
templateUrl: './integratedApplicationsDatatable.html',
controller: 'GenericDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
refreshCallback: '<',
},
});

View file

@ -0,0 +1,156 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i>
{{ $ctrl.titleText }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle> <i class="fa fa-cog" aria-hidden="true"></i> Table settings </span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader">
Table settings
</div>
<div class="menuContent">
<div>
<div class="md-checkbox">
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate">
Refresh rate
</label>
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
<option value="120">2min</option>
<option value="300">5min</option>
</select>
<span>
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
</span>
</div>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
</div>
</div>
</div>
</span>
</div>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('StackName')">
Stack
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'StackName' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'StackName' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('ResourcePool')">
Resource pool
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourcePool' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourcePool' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Image')">
Image
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Image' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Image' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('CPU')">
CPU reservation
<i class="fa fa-sort-numeric-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CPU' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-numeric-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CPU' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Memory')">
Memory reservation
<i class="fa fa-sort-numeric-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Memory' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-numeric-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Memory' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
>
<td>
<a ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool })">{{ item.Name }}</a>
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="$ctrl.isSystemNamespace(item)">system</span>
<span style="margin-left: 5px;" class="label label-primary image-tag" ng-if="!$ctrl.isSystemNamespace(item) && $ctrl.isExternalApplication(item)">external</span>
</td>
<td>{{ item.StackName }}</td>
<td>
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.ResourcePool })">{{ item.ResourcePool }}</a>
</td>
<td>{{ item.Image }}</td>
<td>{{ item.CPU | kubernetesApplicationCPUValue }}</td>
<td>{{ item.Memory | humansize }}</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="6" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="6" class="text-center text-muted">No stack available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,13 @@
angular.module('portainer.kubernetes').component('kubernetesNodeApplicationsDatatable', {
templateUrl: './nodeApplicationsDatatable.html',
controller: 'KubernetesNodeApplicationsDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
refreshCallback: '<',
},
});

View file

@ -0,0 +1,54 @@
import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
angular.module('portainer.docker').controller('KubernetesNodeApplicationsDatatableController', [
'$scope',
'$controller',
'KubernetesNamespaceHelper',
'DatatableService',
function ($scope, $controller, KubernetesNamespaceHelper, DatatableService) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
this.isSystemNamespace = function (item) {
return KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool);
};
this.isExternalApplication = function (item) {
return KubernetesApplicationHelper.isExternalApplication(item);
};
this.$onInit = function () {
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
this.setDefaults();
this.prepareTableFromDataset();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
if (textFilter !== null) {
this.state.textFilter = textFilter;
this.onTextFilterChange();
}
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
if (this.filters && this.filters.state) {
this.filters.state.open = false;
}
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
if (storedSettings !== null) {
this.settings = storedSettings;
this.settings.open = false;
}
this.onSettingsRepeaterChange();
};
},
]);

View file

@ -0,0 +1,164 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Table settings</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader">
Table settings
</div>
<div class="menuContent">
<div>
<div class="md-checkbox">
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate">
Refresh rate
</label>
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
<option value="120">2min</option>
<option value="300">5min</option>
</select>
<span>
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
</span>
</div>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
</div>
</div>
</div>
</span>
</div>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Role')">
Role
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Role' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Role' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Status')">
Status
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('CPU')">
CPU
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CPU' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CPU' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Memory')">
Memory
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Memory' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Memory' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Version')">
Version
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Version' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Version' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('IPAddress')">
IP Address
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IPAddress' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IPAddress' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
>
<td ng-if="$ctrl.isAdmin">
<a ui-sref="kubernetes.cluster.node({ name: item.Name })">
{{ item.Name }}
</a>
</td>
<td ng-if="!$ctrl.isAdmin">
{{ item.Name }}
</td>
<td>{{ item.Role }}</td>
<td
><span class="label label-{{ item.Status | kubernetesNodeStatusColor }}">{{ item.Status }}</span></td
>
<td>{{ item.CPU }}</td>
<td>{{ item.Memory | humansize }}</td>
<td>{{ item.Version }}</td>
<td>{{ item.IPAddress }}</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="7" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="7" class="text-center text-muted">No node available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,13 @@
angular.module('portainer.kubernetes').component('kubernetesNodesDatatable', {
templateUrl: './nodesDatatable.html',
controller: 'GenericDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
refreshCallback: '<',
isAdmin: '<',
},
});

View file

@ -0,0 +1,145 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i>
{{ $ctrl.titleText }}
</div>
<div ng-if="$ctrl.refreshCallback" class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle> <i class="fa fa-cog" aria-hidden="true"></i> Table settings </span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader">
Table settings
</div>
<div class="menuContent">
<div>
<div class="md-checkbox">
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate">
Refresh rate
</label>
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
<option value="120">2min</option>
<option value="300">5min</option>
</select>
<span>
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
</span>
</div>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
</div>
</div>
</div>
</span>
</div>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('StackName')">
Stack
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'StackName' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'StackName' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Image')">
Image
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Image' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Image' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('CPU')">
CPU reservation
<i class="fa fa-sort-numeric-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CPU' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-numeric-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CPU' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Memory')">
Memory reservation
<i class="fa fa-sort-numeric-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Memory' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-numeric-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Memory' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
>
<td>
<a ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool })">{{ item.Name }}</a>
<span style="margin-left: 5px;" class="label label-primary image-tag" ng-if="$ctrl.isExternalApplication(item)">external</span>
</td>
<td>{{ item.StackName }}</td>
<td>{{ item.Image }}</td>
<td>{{ item.CPU | kubernetesApplicationCPUValue }}</td>
<td>{{ item.Memory | humansize }}</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="5" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="5" class="text-center text-muted">No application available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,13 @@
angular.module('portainer.kubernetes').component('kubernetesResourcePoolApplicationsDatatable', {
templateUrl: './resourcePoolApplicationsDatatable.html',
controller: 'KubernetesResourcePoolApplicationsDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
refreshCallback: '<',
},
});

View file

@ -0,0 +1,47 @@
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
angular.module('portainer.docker').controller('KubernetesResourcePoolApplicationsDatatableController', [
'$scope',
'$controller',
'DatatableService',
function ($scope, $controller, DatatableService) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
this.isExternalApplication = function (item) {
return KubernetesApplicationHelper.isExternalApplication(item);
};
this.$onInit = function () {
this.setDefaults();
this.prepareTableFromDataset();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
if (textFilter !== null) {
this.state.textFilter = textFilter;
this.onTextFilterChange();
}
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
if (this.filters && this.filters.state) {
this.filters.state.open = false;
}
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
if (storedSettings !== null) {
this.settings = storedSettings;
this.settings.open = false;
}
this.onSettingsRepeaterChange();
};
},
]);

View file

@ -0,0 +1,160 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
<span class="small text-muted" style="float: left; margin-top: 5px;" ng-if="$ctrl.isAdmin && !$ctrl.settings.showSystem">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
System resources are hidden, this can be changed in the table settings.
</span>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Table settings</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader">
Table settings
</div>
<div class="menuContent">
<div>
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
<input id="setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
<label for="setting_show_system">Show system resources</label>
</div>
<div class="md-checkbox">
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate">
Refresh rate
</label>
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
<option value="120">2min</option>
<option value="300">5min</option>
</select>
<span>
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
</span>
</div>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
</div>
</div>
</div>
</span>
</div>
</div>
<div ng-if="$ctrl.isAdmin" class="actionBar">
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.resourcePools.new">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add resource pool
</button>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
<span ng-if="$ctrl.isAdmin" class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Namespace.Name')">
Name
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Namespace.Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Namespace.Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Quota')">
Quota
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Quota' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Quota' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('CreationDate')">
Created
<i class="fa fa-sort-numeric-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CreationDate' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-numeric-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CreationDate' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th ng-if="$ctrl.isAdmin">
Actions
</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | filter:$ctrl.isDisplayed | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
ng-class="{ active: item.Checked }"
>
<td>
<span ng-if="$ctrl.isAdmin" class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" ng-disabled="$ctrl.disableRemove(item)" />
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.Namespace.Name })">{{ item.Namespace.Name }}</a>
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="$ctrl.isSystemNamespace(item)">system</span>
</td>
<td> <i class="fa {{ item.Quota ? 'fa-toggle-on' : 'fa-toggle-off' }}" aria-hidden="true" style="margin-right: 2px;"></i> {{ item.Quota ? 'Yes' : 'No' }} </td>
<td>{{ item.CreationDate | getisodate }} {{ item.Namespace.ResourcePoolOwner ? 'by ' + item.Namespace.ResourcePoolOwner : '' }}</td>
<td ng-if="$ctrl.isAdmin">
<a ng-if="$ctrl.canManageAccess(item)" ui-sref="kubernetes.resourcePools.resourcePool.access({id: item.Namespace.Name})">
<i class="fa fa-users" aria-hidden="true"></i> Manage access
</a>
<span ng-if="!$ctrl.canManageAccess(item)">-</span>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="4" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="4" class="text-center text-muted">No resource pool available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,14 @@
angular.module('portainer.kubernetes').component('kubernetesResourcePoolsDatatable', {
templateUrl: './resourcePoolsDatatable.html',
controller: 'KubernetesResourcePoolsDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
removeAction: '<',
refreshCallback: '<',
},
});

View file

@ -0,0 +1,77 @@
angular.module('portainer.docker').controller('KubernetesResourcePoolsDatatableController', [
'$scope',
'$controller',
'Authentication',
'KubernetesNamespaceHelper',
'DatatableService',
function ($scope, $controller, Authentication, KubernetesNamespaceHelper, DatatableService) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
var ctrl = this;
this.settings = Object.assign(this.settings, {
showSystem: false,
});
this.onSettingsShowSystemChange = function () {
DatatableService.setDataTableSettings(this.tableKey, this.settings);
};
this.canManageAccess = function (item) {
return item.Namespace.Name !== 'default' && !this.isSystemNamespace(item);
};
this.disableRemove = function (item) {
return KubernetesNamespaceHelper.isSystemNamespace(item.Namespace.Name) || item.Namespace.Name === 'default';
};
this.isSystemNamespace = function (item) {
return KubernetesNamespaceHelper.isSystemNamespace(item.Namespace.Name);
};
this.isDisplayed = function (item) {
return !ctrl.isSystemNamespace(item) || (ctrl.settings.showSystem && ctrl.isAdmin);
};
/**
* Do not allow system namespaces to be selected
*/
this.allowSelection = function (item) {
return !this.disableRemove(item);
};
this.$onInit = function () {
this.isAdmin = Authentication.isAdmin();
this.setDefaults();
this.prepareTableFromDataset();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
if (textFilter !== null) {
this.state.textFilter = textFilter;
this.onTextFilterChange();
}
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
if (this.filters && this.filters.state) {
this.filters.state.open = false;
}
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
if (storedSettings !== null) {
this.settings = storedSettings;
this.settings.open = false;
}
this.onSettingsRepeaterChange();
};
},
]);

View file

@ -0,0 +1,192 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
<span class="small text-muted" style="float: left; margin-top: 5px;" ng-if="$ctrl.isAdmin && !$ctrl.settings.showSystem">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
System resources are hidden, this can be changed in the table settings.
</span>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Table settings</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader">
Table settings
</div>
<div class="menuContent">
<div>
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
<input id="setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
<label for="setting_show_system">Show system resources</label>
</div>
<div class="md-checkbox">
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate">
Refresh rate
</label>
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
<option value="120">2min</option>
<option value="300">5min</option>
</select>
<span>
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
</span>
</div>
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
</div>
</div>
</div>
</span>
</div>
</div>
<div class="actionBar">
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('PersistentVolumeClaim.Name')">
Name
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'PersistentVolumeClaim.Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'PersistentVolumeClaim.Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('ResourcePool.Namespace.Name')">
Resource pool
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourcePool.Namespace.Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourcePool.Namespace.Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('PersistentVolumeClaim.Applications[0].Name')">
Used by
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'PersistentVolumeClaim.Applications[0].Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'PersistentVolumeClaim.Applications[0].Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('PersistentVolumeClaim.StorageClass.Name')">
Storage
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'PersistentVolumeClaim.StorageClass.Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'PersistentVolumeClaim.StorageClass.Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('PersistentVolumeClaim.Storage')">
Size
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'PersistentVolumeClaim.Storage' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'PersistentVolumeClaim.Storage' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('PersistentVolumeClaim.CreationDate')">
Created
<i class="fa fa-sort-numeric-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'PersistentVolumeClaim.CreationDate' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-numeric-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'PersistentVolumeClaim.CreationDate' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | filter:$ctrl.isDisplayed | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
ng-class="{ active: item.Checked }"
>
<td>
<span class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" ng-disabled="$ctrl.disableRemove(item)" />
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="kubernetes.volumes.volume({ namespace: item.ResourcePool.Namespace.Name, name: item.PersistentVolumeClaim.Name })">{{
item.PersistentVolumeClaim.Name
}}</a>
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item)">system</span>
<span class="label label-primary image-tag label-margins" ng-if="!$ctrl.isSystemNamespace(item) && $ctrl.isExternalVolume(item)">external</span>
<span class="label label-warning image-tag label-margins" ng-if="!$ctrl.isSystemNamespace(item) && !$ctrl.isUsed(item)">unused</span>
</td>
<td>
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.ResourcePool.Namespace.Name })">{{ item.ResourcePool.Namespace.Name }}</a>
</td>
<td>
<a
ng-if="item.Applications.length"
ui-sref="kubernetes.applications.application({ name: item.Applications[0].Name, namespace: item.ResourcePool.Namespace.Name })"
>{{ item.Applications[0].Name }}</a
>
<span ng-if="!item.Applications.length">-</span>
</td>
<td>
{{ item.PersistentVolumeClaim.StorageClass.Name }}
</td>
<td>
{{ item.PersistentVolumeClaim.Storage }}
</td>
<td>
{{ item.PersistentVolumeClaim.CreationDate | getisodate }}
{{ item.PersistentVolumeClaim.ApplicationOwner ? 'by ' + item.PersistentVolumeClaim.ApplicationOwner : '' }}
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="5" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="5" class="text-center text-muted">No volume available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,14 @@
angular.module('portainer.kubernetes').component('kubernetesVolumesDatatable', {
templateUrl: './volumesDatatable.html',
controller: 'KubernetesVolumesDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
removeAction: '<',
refreshCallback: '<',
},
});

View file

@ -0,0 +1,91 @@
import angular from 'angular';
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
// TODO: review - refactor to use `extends GenericDatatableController`
class KubernetesVolumesDatatableController {
/* @ngInject */
constructor($async, $controller, Authentication, KubernetesNamespaceHelper, DatatableService) {
this.$async = $async;
this.$controller = $controller;
this.Authentication = Authentication;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.DatatableService = DatatableService;
this.onInit = this.onInit.bind(this);
this.allowSelection = this.allowSelection.bind(this);
this.isDisplayed = this.isDisplayed.bind(this);
}
onSettingsShowSystemChange() {
this.DatatableService.setDataTableSettings(this.tableKey, this.settings);
}
disableRemove(item) {
return this.isSystemNamespace(item) || this.isUsed(item);
}
isUsed(item) {
return KubernetesVolumeHelper.isUsed(item);
}
isSystemNamespace(item) {
return this.KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool.Namespace.Name);
}
isDisplayed(item) {
return !this.isSystemNamespace(item) || this.showSystem;
}
isExternalVolume(item) {
return KubernetesVolumeHelper.isExternalVolume(item);
}
allowSelection(item) {
return !this.disableRemove(item);
}
async onInit() {
this.setDefaults();
this.prepareTableFromDataset();
this.isAdmin = this.Authentication.isAdmin();
this.state.orderBy = this.orderBy;
var storedOrder = this.DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
var textFilter = this.DatatableService.getDataTableTextFilters(this.tableKey);
if (textFilter !== null) {
this.state.textFilter = textFilter;
this.onTextFilterChange();
}
var storedFilters = this.DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
if (this.filters && this.filters.state) {
this.filters.state.open = false;
}
var storedSettings = this.DatatableService.getDataTableSettings(this.tableKey);
if (storedSettings !== null) {
this.settings = storedSettings;
this.settings.open = false;
}
this.onSettingsRepeaterChange();
this.settings.showSystem = false;
}
$onInit() {
const ctrl = angular.extend({}, this.$controller('GenericDatatableController'), this);
angular.extend(this, ctrl);
return this.$async(this.onInit);
}
}
export default KubernetesVolumesDatatableController;
angular.module('portainer.kubernetes').controller('KubernetesVolumesDatatableController', KubernetesVolumesDatatableController);

View file

@ -0,0 +1,9 @@
<information-panel title-text="Information">
<span class="small">
<p class="text-muted">
<i class="fa fa-flask orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Kubernetes support in Portainer is now in RC stage. Contribute and share your feedback in
<a href="https://github.com/portainer/kubernetes-beta" target="_blank">our official repository</a>.
</p>
</span>
</information-panel>

View file

@ -0,0 +1,3 @@
angular.module('portainer.kubernetes').component('kubernetesFeedbackPanel', {
templateUrl: './feedbackPanel.html',
});

View file

@ -0,0 +1,97 @@
<ng-form name="kubernetesConfigurationDataCreationForm">
<div class="col-sm-12 form-section-title" style="margin-top: 10px;">
Data
</div>
<div class="form-group">
<div class="col-sm-12">
<p>
<a class="small interactive" ng-if="$ctrl.formValues.IsSimple" ng-click="$ctrl.formValues.IsSimple = false">
<i class="fa fa-list-ol space-right" aria-hidden="true"></i> Advanced mode
</a>
<a class="small interactive" ng-if="!$ctrl.formValues.IsSimple" ng-click="$ctrl.formValues.IsSimple = true">
<i class="fa fa-edit space-right" aria-hidden="true"></i> Simple mode
</a>
</p>
</div>
<div class="col-sm-12 small text-muted" ng-if="$ctrl.formValues.IsSimple">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Switch to advanced mode to copy and paste multiple key/values
</div>
<div class="col-sm-12 small text-muted" ng-if="!$ctrl.formValues.IsSimple">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Generate a configuration entry per line, use YAML format
</div>
</div>
<div class="form-group" ng-if="$ctrl.formValues.IsSimple">
<div class="col-sm-12">
<button type="button" class="btn btn-sm btn-default" style="margin-left: 0;" ng-click="$ctrl.addEntry()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> Create entry
</button>
<button type="button" class="btn btn-sm btn-default" ngf-select="$ctrl.addEntryFromFile($file)" style="margin-left: 0;">
<i class="fa fa-file-upload" aria-hidden="true"></i> Create entry from file
</button>
</div>
</div>
<div ng-repeat="(index, entry) in $ctrl.formValues.Data" ng-if="$ctrl.formValues.IsSimple">
<div class="form-group">
<label for="configuration_data_key_{{ index }}" class="col-sm-1 control-label text-left">Key</label>
<div class="col-sm-11">
<input
type="text"
class="form-control"
id="configuration_data_key_{{ index }}"
name="configuration_data_key_{{ index }}"
ng-model="$ctrl.formValues.Data[index].Key"
required
ng-change="$ctrl.onChangeKey()"
/>
</div>
<div
class="col-sm-11 small text-warning"
style="margin-top: 5px;"
ng-show="kubernetesConfigurationDataCreationForm['configuration_data_key_' + index].$invalid || $ctrl.state.duplicateKeys[index] !== undefined"
>
<ng-messages for="kubernetesConfigurationDataCreationForm['configuration_data_key_' + index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</ng-messages>
<p ng-if="$ctrl.state.duplicateKeys[index] !== undefined"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This key is already defined.</p>
</div>
</div>
<div class="form-group" ng-if="$ctrl.formValues.IsSimple">
<label for="configuration_data_value_{{ index }}" class="col-sm-1 control-label text-left">Value</label>
<div class="col-sm-11">
<textarea
class="form-control"
rows="5"
id="configuration_data_value_{{ index }}"
name="configuration_data_value_{{ index }}"
ng-model="$ctrl.formValues.Data[index].Value"
required
></textarea>
</div>
<div class="col-sm-11 small text-warning" style="margin-top: 5px;" ng-show="kubernetesConfigurationDataCreationForm['configuration_data_value_' + index].$invalid">
<ng-messages for="kubernetesConfigurationDataCreationForm['configuration_data_value_' + index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</ng-messages>
</div>
</div>
<div class="form-group" ng-if="$ctrl.formValues.IsSimple">
<div class="col-sm-1"></div>
<div class="col-sm-11">
<button type="button" class="btn btn-sm btn-danger" style="margin-left: 0;" ng-click="$ctrl.removeEntry(index)">
<i class="fa fa-trash" aria-hidden="true"></i> Remove entry
</button>
</div>
</div>
</div>
<div class="form-group" ng-if="!$ctrl.formValues.IsSimple">
<input type="text" ng-model="$ctrl.formValues.DataYaml" required style="display: none;" />
<code-editor identifier="kubernetes-configuration-editor" value="$ctrl.formValues.DataYaml" read-only="false" yml="true" on-change="($ctrl.editorUpdate)"></code-editor>
</div>
</ng-form>

View file

@ -0,0 +1,8 @@
angular.module('portainer.kubernetes').component('kubernetesConfigurationData', {
templateUrl: './kubernetesConfigurationData.html',
controller: 'KubernetesConfigurationDataController',
bindings: {
formValues: '=',
isValid: '=',
},
});

View file

@ -0,0 +1,68 @@
import angular from 'angular';
import _ from 'lodash-es';
import { KubernetesConfigurationFormValuesDataEntry } from 'Kubernetes/models/configuration/formvalues';
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
class KubernetesConfigurationDataController {
/* @ngInject */
constructor($async) {
this.$async = $async;
this.editorUpdate = this.editorUpdate.bind(this);
this.editorUpdateAsync = this.editorUpdateAsync.bind(this);
this.onFileLoad = this.onFileLoad.bind(this);
this.onFileLoadAsync = this.onFileLoadAsync.bind(this);
}
onChangeKey() {
this.state.duplicateKeys = KubernetesFormValidationHelper.getDuplicates(_.map(this.formValues.Data, (data) => data.Key));
this.isValid = Object.keys(this.state.duplicateKeys).length === 0;
}
addEntry() {
this.formValues.Data.push(new KubernetesConfigurationFormValuesDataEntry());
}
removeEntry(index) {
this.formValues.Data.splice(index, 1);
this.onChangeKey();
}
async editorUpdateAsync(cm) {
this.formValues.DataYaml = cm.getValue();
}
editorUpdate(cm) {
return this.$async(this.editorUpdateAsync, cm);
}
async onFileLoadAsync(event) {
const entry = new KubernetesConfigurationFormValuesDataEntry();
entry.Key = event.target.fileName;
entry.Value = event.target.result;
this.formValues.Data.push(entry);
this.onChangeKey();
}
onFileLoad(event) {
return this.$async(this.onFileLoadAsync, event);
}
addEntryFromFile(file) {
if (file) {
const temporaryFileReader = new FileReader();
temporaryFileReader.fileName = file.name;
temporaryFileReader.onload = this.onFileLoad;
temporaryFileReader.readAsText(file);
}
}
$onInit() {
this.state = {
duplicateKeys: {},
};
}
}
export default KubernetesConfigurationDataController;
angular.module('portainer.kubernetes').controller('KubernetesConfigurationDataController', KubernetesConfigurationDataController);

View file

@ -0,0 +1,18 @@
<li class="sidebar-list">
<a ui-sref="kubernetes.dashboard" ui-sref-active="active">Dashboard <span class="menu-icon fa fa-tachometer-alt fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="kubernetes.resourcePools" ui-sref-active="active">Resource pools <span class="menu-icon fa fa-layer-group fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="kubernetes.applications" ui-sref-active="active">Applications <span class="menu-icon fa fa-laptop-code fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="kubernetes.configurations" ui-sref-active="active">Configurations <span class="menu-icon fa fa-file-code fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="kubernetes.volumes" ui-sref-active="active">Volumes <span class="menu-icon fa fa-database fa-fw"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="kubernetes.cluster" ui-sref-active="active">Cluster <span class="menu-icon fa fa-server fa-fw"></span></a>
</li>

View file

@ -0,0 +1,6 @@
angular.module('portainer.kubernetes').component('kubernetesSidebarContent', {
templateUrl: './kubernetesSidebarContent.html',
bindings: {
adminAccess: '<',
},
});

View file

@ -0,0 +1,32 @@
<div class="row">
<div class="col-sm-12 form-section-title">
Resource reservation
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
<p>
{{ $ctrl.description }}
</p>
</span>
</div>
<div class="form-group" ng-if="$ctrl.memoryLimit !== 0">
<label for="memory-usage" class="col-sm-3 col-lg-2 control-label text-left">
Memory reservation
</label>
<div class="col-sm-9" style="margin-top: 4px;">
<uib-progressbar animate="false" value="$ctrl.memoryUsage" type="{{ $ctrl.memoryUsage | kubernetesUsageLevelInfo }}">
<b style="white-space: nowrap;"> {{ $ctrl.memory }} / {{ $ctrl.memoryLimit }} MB - {{ $ctrl.memoryUsage }}% </b>
</uib-progressbar>
</div>
</div>
<div class="form-group" ng-if="$ctrl.cpuLimit !== 0">
<label for="cpu-usage" class="col-sm-3 col-lg-2 control-label text-left">
CPU reservation
</label>
<div class="col-sm-9" style="margin-top: 4px;">
<uib-progressbar animate="false" value="$ctrl.cpuUsage" type="{{ $ctrl.cpuUsage | kubernetesUsageLevelInfo }}">
<b style="white-space: nowrap;"> {{ $ctrl.cpu | kubernetesApplicationCPUValue }} / {{ $ctrl.cpuLimit }} - {{ $ctrl.cpuUsage }}% </b>
</uib-progressbar>
</div>
</div>
</div>

View file

@ -0,0 +1,11 @@
angular.module('portainer.kubernetes').component('kubernetesResourceReservation', {
templateUrl: './resourceReservation.html',
controller: 'KubernetesResourceReservationController',
bindings: {
description: '@',
cpu: '<',
cpuLimit: '<',
memory: '<',
memoryLimit: '<',
},
});

View file

@ -0,0 +1,23 @@
import angular from 'angular';
class KubernetesResourceReservationController {
usageValues() {
if (this.cpuLimit) {
this.cpuUsage = Math.round((this.cpu / this.cpuLimit) * 100);
}
if (this.memoryLimit) {
this.memoryUsage = Math.round((this.memory / this.memoryLimit) * 100);
}
}
$onInit() {
this.usageValues();
}
$onChanges() {
this.usageValues();
}
}
export default KubernetesResourceReservationController;
angular.module('portainer.kubernetes').controller('KubernetesResourceReservationController', KubernetesResourceReservationController);

View file

@ -0,0 +1,10 @@
<rd-header ng-if="$ctrl.viewReady">
<rd-header-title title-text="{{ $ctrl.title }}">
<a data-toggle="tooltip" title="refresh the view" ui-sref="{{ $ctrl.state }}" ui-sref-opts="{reload: true}" ng-if="$ctrl.viewReady">
<i class="fa fa-sm fa-sync" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>
<ng-transclude></ng-transclude>
</rd-header-content>
</rd-header>

View file

@ -0,0 +1,9 @@
angular.module('portainer.kubernetes').component('kubernetesViewHeader', {
templateUrl: './viewHeader.html',
transclude: true,
bindings: {
viewReady: '<',
title: '@',
state: '@',
},
});

View file

@ -0,0 +1,8 @@
<div class="loading-view-area" ng-if="!$ctrl.viewReady">
<div class="sk-fold sk-center">
<div class="sk-fold-cube"></div>
<div class="sk-fold-cube"></div>
<div class="sk-fold-cube"></div>
<div class="sk-fold-cube"></div>
</div>
</div>

View file

@ -0,0 +1,6 @@
angular.module('portainer.kubernetes').component('kubernetesViewLoading', {
templateUrl: './viewLoading.html',
bindings: {
viewReady: '<',
},
});

View file

@ -0,0 +1,7 @@
<div>
<code-editor identifier="application-details-yaml" read-only="true" value="$ctrl.data"></code-editor>
<div style="margin: 15px;">
<span class="btn btn-primary btn-sm" ng-click="$ctrl.copyYAML()"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy to clipboard</span>
<span id="copyNotificationYAML" style="margin-left: 7px; display: none; color: #23ae89;" class="small"> <i class="fa fa-check" aria-hidden="true"></i> copied </span>
</div>
</div>

View file

@ -0,0 +1,8 @@
angular.module('portainer.kubernetes').component('kubernetesYamlInspector', {
templateUrl: './yamlInspector.html',
controller: 'KubernetesYamlInspectorController',
bindings: {
key: '@',
data: '<',
},
});

View file

@ -0,0 +1,17 @@
import angular from 'angular';
class KubernetesYamlInspectorController {
/* @ngInject */
constructor(clipboard) {
this.clipboard = clipboard;
}
copyYAML() {
this.clipboard.copyText(this.data);
$('#copyNotificationYAML').show().fadeOut(2500);
}
}
export default KubernetesYamlInspectorController;
angular.module('portainer.kubernetes').controller('KubernetesYamlInspectorController', KubernetesYamlInspectorController);

View file

@ -0,0 +1,302 @@
import _ from 'lodash-es';
import filesizeParser from 'filesize-parser';
import {
KubernetesApplication,
KubernetesApplicationConfigurationVolume,
KubernetesApplicationDataAccessPolicies,
KubernetesApplicationDeploymentTypes,
KubernetesApplicationPersistedFolder,
KubernetesApplicationPublishingTypes,
KubernetesApplicationTypes,
KubernetesPortainerApplicationNameLabel,
KubernetesPortainerApplicationNote,
KubernetesPortainerApplicationOwnerLabel,
KubernetesPortainerApplicationStackNameLabel,
} from 'Kubernetes/models/application/models';
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
import { KubernetesApplicationFormValues } from 'Kubernetes/models/application/formValues';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import KubernetesDeploymentConverter from 'Kubernetes/converters/deployment';
import KubernetesDaemonSetConverter from 'Kubernetes/converters/daemonSet';
import KubernetesStatefulSetConverter from 'Kubernetes/converters/statefulSet';
import KubernetesServiceConverter from 'Kubernetes/converters/service';
import KubernetesPersistentVolumeClaimConverter from 'Kubernetes/converters/persistentVolumeClaim';
import PortainerError from 'Portainer/error';
class KubernetesApplicationConverter {
static applicationCommon(res, data, service) {
res.Id = data.metadata.uid;
res.Name = data.metadata.name;
res.StackName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationStackNameLabel] || '-' : '-';
res.ApplicationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationOwnerLabel] || '' : '';
res.Note = data.metadata.annotations ? data.metadata.annotations[KubernetesPortainerApplicationNote] || '' : '';
res.ApplicationName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationNameLabel] || res.Name : res.Name;
res.ResourcePool = data.metadata.namespace;
res.Image = data.spec.template.spec.containers[0].image;
res.CreationDate = data.metadata.creationTimestamp;
res.Pods = data.Pods;
res.Env = data.spec.template.spec.containers[0].env;
const limits = {
Cpu: 0,
Memory: 0,
};
res.Limits = _.reduce(
data.spec.template.spec.containers,
(acc, item) => {
if (item.resources.limits && item.resources.limits.cpu) {
acc.Cpu += KubernetesResourceReservationHelper.parseCPU(item.resources.limits.cpu);
}
if (item.resources.limits && item.resources.limits.memory) {
acc.Memory += filesizeParser(item.resources.limits.memory, { base: 10 });
}
return acc;
},
limits
);
const requests = {
Cpu: 0,
Memory: 0,
};
res.Requests = _.reduce(
data.spec.template.spec.containers,
(acc, item) => {
if (item.resources.requests && item.resources.requests.cpu) {
acc.Cpu += KubernetesResourceReservationHelper.parseCPU(item.resources.requests.cpu);
}
if (item.resources.requests && item.resources.requests.memory) {
acc.Memory += filesizeParser(item.resources.requests.memory, { base: 10 });
}
return acc;
},
requests
);
if (service) {
const serviceType = service.spec.type;
res.ServiceType = serviceType;
res.ServiceId = service.metadata.uid;
res.ServiceName = service.metadata.name;
if (serviceType === KubernetesServiceTypes.LOAD_BALANCER) {
if (service.status.loadBalancer.ingress && service.status.loadBalancer.ingress.length > 0) {
res.LoadBalancerIPAddress = service.status.loadBalancer.ingress[0].ip || service.status.loadBalancer.ingress[0].hostname;
}
}
const ports = _.concat(..._.map(data.spec.template.spec.containers, (container) => container.ports));
res.PublishedPorts = service.spec.ports;
_.forEach(res.PublishedPorts, (publishedPort) => {
if (isNaN(publishedPort.targetPort)) {
const targetPort = _.find(ports, { name: publishedPort.targetPort });
if (targetPort) {
publishedPort.targetPort = targetPort.containerPort;
}
}
});
}
res.Volumes = data.spec.template.spec.volumes ? data.spec.template.spec.volumes : [];
// TODO: review
// this if() fixs direct use of PVC reference inside spec.template.spec.containers[0].volumeMounts
// instead of referencing the PVC the "good way" using spec.template.spec.volumes array
// Basically it creates an "in-memory" reference for the PVC, as if it was saved in
// spec.template.spec.volumes and retrieved from here.
//
// FIX FOR SFS ONLY ; as far as we know it's not possible to do this with DEPLOYMENTS/DAEMONSETS
//
// This may lead to destructing behaviours when we will allow external apps to be edited.
// E.G. if we try to generate the formValues and patch the app, SFS reference will be created under
// spec.template.spec.volumes and not be referenced directly inside spec.template.spec.containers[0].volumeMounts
// As we preserve original SFS name and try to build around it, it SHOULD be fine, but we definitely need to test this
// before allowing external apps modification
if (data.spec.volumeClaimTemplates) {
const vcTemplates = _.map(data.spec.volumeClaimTemplates, (vc) => {
return {
name: vc.metadata.name,
persistentVolumeClaim: { claimName: vc.metadata.name },
};
});
const inexistingPVC = _.filter(vcTemplates, (vc) => {
return !_.find(res.Volumes, { persistentVolumeClaim: { claimName: vc.persistentVolumeClaim.claimName } });
});
res.Volumes = _.concat(res.Volumes, inexistingPVC);
}
const persistedFolders = _.filter(res.Volumes, (volume) => volume.persistentVolumeClaim || volume.hostPath);
res.PersistedFolders = _.map(persistedFolders, (volume) => {
const matchingVolumeMount = _.find(data.spec.template.spec.containers[0].volumeMounts, { name: volume.name });
if (matchingVolumeMount) {
const persistedFolder = new KubernetesApplicationPersistedFolder();
persistedFolder.MountPath = matchingVolumeMount.mountPath;
if (volume.persistentVolumeClaim) {
persistedFolder.PersistentVolumeClaimName = volume.persistentVolumeClaim.claimName;
} else {
persistedFolder.HostPath = volume.hostPath.path;
}
return persistedFolder;
}
});
res.PersistedFolders = _.without(res.PersistedFolders, undefined);
res.ConfigurationVolumes = _.reduce(
data.spec.template.spec.volumes,
(acc, volume) => {
if (volume.configMap || volume.secret) {
const matchingVolumeMount = _.find(data.spec.template.spec.containers[0].volumeMounts, { name: volume.name });
if (matchingVolumeMount) {
let items = [];
let configurationName = '';
if (volume.configMap) {
items = volume.configMap.items;
configurationName = volume.configMap.name;
} else {
items = volume.secret.items;
configurationName = volume.secret.secretName;
}
if (!items) {
const configurationVolume = new KubernetesApplicationConfigurationVolume();
configurationVolume.fileMountPath = matchingVolumeMount.mountPath;
configurationVolume.rootMountPath = matchingVolumeMount.mountPath;
configurationVolume.configurationName = configurationName;
acc.push(configurationVolume);
} else {
_.forEach(items, (item) => {
const configurationVolume = new KubernetesApplicationConfigurationVolume();
configurationVolume.fileMountPath = matchingVolumeMount.mountPath + '/' + item.path;
configurationVolume.rootMountPath = matchingVolumeMount.mountPath;
configurationVolume.configurationKey = item.key;
configurationVolume.configurationName = configurationName;
acc.push(configurationVolume);
});
}
}
}
return acc;
},
[]
);
}
static apiDeploymentToApplication(data, service) {
const res = new KubernetesApplication();
KubernetesApplicationConverter.applicationCommon(res, data, service);
res.ApplicationType = KubernetesApplicationTypes.DEPLOYMENT;
res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED;
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED;
res.RunningPodsCount = data.status.availableReplicas || data.status.replicas - data.status.unavailableReplicas || 0;
res.TotalPodsCount = data.spec.replicas;
return res;
}
static apiDaemonSetToApplication(data, service) {
const res = new KubernetesApplication();
KubernetesApplicationConverter.applicationCommon(res, data, service);
res.ApplicationType = KubernetesApplicationTypes.DAEMONSET;
res.DeploymentType = KubernetesApplicationDeploymentTypes.GLOBAL;
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED;
res.RunningPodsCount = data.status.numberAvailable || data.status.desiredNumberScheduled - data.status.numberUnavailable || 0;
res.TotalPodsCount = data.status.desiredNumberScheduled;
return res;
}
static apiStatefulSetToapplication(data, service) {
const res = new KubernetesApplication();
KubernetesApplicationConverter.applicationCommon(res, data, service);
res.ApplicationType = KubernetesApplicationTypes.STATEFULSET;
res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED;
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.ISOLATED;
res.RunningPodsCount = data.status.readyReplicas || 0;
res.TotalPodsCount = data.spec.replicas;
res.HeadlessServiceName = data.spec.serviceName;
return res;
}
static applicationToFormValues(app, resourcePools, configurations, persistentVolumeClaims) {
const res = new KubernetesApplicationFormValues();
res.ApplicationType = app.ApplicationType;
res.ResourcePool = _.find(resourcePools, ['Namespace.Name', app.ResourcePool]);
res.Name = app.Name;
res.StackName = app.StackName;
res.ApplicationOwner = app.ApplicationOwner;
res.Image = app.Image;
res.ReplicaCount = app.TotalPodsCount;
res.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(app.Limits.Memory);
res.CpuLimit = app.Limits.Cpu;
res.DeploymentType = app.DeploymentType;
res.DataAccessPolicy = app.DataAccessPolicy;
res.EnvironmentVariables = KubernetesApplicationHelper.generateEnvVariablesFromEnv(app.Env);
res.PersistedFolders = KubernetesApplicationHelper.generatePersistedFoldersFormValuesFromPersistedFolders(app.PersistedFolders, persistentVolumeClaims); // generate from PVC and app.PersistedFolders
res.Configurations = KubernetesApplicationHelper.generateConfigurationFormValuesFromEnvAndVolumes(app.Env, app.ConfigurationVolumes, configurations);
if (app.ServiceType === KubernetesServiceTypes.LOAD_BALANCER) {
res.PublishingType = KubernetesApplicationPublishingTypes.LOAD_BALANCER;
} else if (app.ServiceType === KubernetesServiceTypes.NODE_PORT) {
res.PublishingType = KubernetesApplicationPublishingTypes.CLUSTER;
} else {
res.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL;
}
res.PublishedPorts = KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(app.ServiceType, app.PublishedPorts);
return res;
}
static applicationFormValuesToApplication(formValues) {
const claims = KubernetesPersistentVolumeClaimConverter.applicationFormValuesToVolumeClaims(formValues);
const rwx = _.find(claims, (item) => _.includes(item.StorageClass.AccessModes, 'RWX')) !== undefined;
const deployment =
(formValues.DeploymentType === KubernetesApplicationDeploymentTypes.REPLICATED &&
(claims.length === 0 || (claims.length > 0 && formValues.DataAccessPolicy === KubernetesApplicationDataAccessPolicies.SHARED))) ||
formValues.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT;
const statefulSet =
(formValues.DeploymentType === KubernetesApplicationDeploymentTypes.REPLICATED &&
claims.length > 0 &&
formValues.DataAccessPolicy === KubernetesApplicationDataAccessPolicies.ISOLATED) ||
formValues.ApplicationType === KubernetesApplicationTypes.STATEFULSET;
const daemonSet =
(formValues.DeploymentType === KubernetesApplicationDeploymentTypes.GLOBAL &&
(claims.length === 0 || (claims.length > 0 && formValues.DataAccessPolicy === KubernetesApplicationDataAccessPolicies.SHARED && rwx))) ||
formValues.ApplicationType === KubernetesApplicationTypes.DAEMONSET;
let app;
if (deployment) {
app = KubernetesDeploymentConverter.applicationFormValuesToDeployment(formValues, claims);
} else if (statefulSet) {
app = KubernetesStatefulSetConverter.applicationFormValuesToStatefulSet(formValues, claims);
} else if (daemonSet) {
app = KubernetesDaemonSetConverter.applicationFormValuesToDaemonSet(formValues, claims);
} else {
throw new PortainerError('Unable to determine which association to use');
}
let headlessService;
if (statefulSet) {
headlessService = KubernetesServiceConverter.applicationFormValuesToHeadlessService(formValues);
}
let service = KubernetesServiceConverter.applicationFormValuesToService(formValues);
if (!service.Ports.length) {
service = undefined;
}
return [app, headlessService, service, claims];
}
}
export default KubernetesApplicationConverter;

View file

@ -0,0 +1,81 @@
import _ from 'lodash-es';
import YAML from 'yaml';
import { KubernetesConfigMap } from 'Kubernetes/models/config-map/models';
import { KubernetesConfigMapCreatePayload, KubernetesConfigMapUpdatePayload } from 'Kubernetes/models/config-map/payloads';
import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models';
class KubernetesConfigMapConverter {
/**
* API ConfigMap to front ConfigMap
*/
static apiToConfigMap(data, yaml) {
const res = new KubernetesConfigMap();
res.Id = data.metadata.uid;
res.Name = data.metadata.name;
res.Namespace = data.metadata.namespace;
res.ConfigurationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
res.CreationDate = data.metadata.creationTimestamp;
res.Yaml = yaml ? yaml.data : '';
res.Data = data.data;
return res;
}
/**
* Generate a default ConfigMap Model
* with ID = 0 (showing it's a default)
* but setting his Namespace and Name
*/
static defaultConfigMap(namespace, name) {
const res = new KubernetesConfigMap();
res.Name = name;
res.Namespace = namespace;
return res;
}
/**
* CREATE payload
*/
static createPayload(data) {
const res = new KubernetesConfigMapCreatePayload();
res.metadata.name = data.Name;
res.metadata.namespace = data.Namespace;
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = data.ConfigurationOwner;
res.data = data.Data;
return res;
}
/**
* UPDATE payload
*/
static updatePayload(data) {
const res = new KubernetesConfigMapUpdatePayload();
res.metadata.uid = data.Id;
res.metadata.name = data.Name;
res.metadata.namespace = data.Namespace;
res.data = data.Data;
return res;
}
static configurationFormValuesToConfigMap(formValues) {
const res = new KubernetesConfigMap();
res.Id = formValues.Id;
res.Name = formValues.Name;
res.Namespace = formValues.ResourcePool.Namespace.Name;
res.ConfigurationOwner = formValues.ConfigurationOwner;
if (formValues.IsSimple) {
res.Data = _.reduce(
formValues.Data,
(acc, entry) => {
acc[entry.Key] = entry.Value;
return acc;
},
{}
);
} else {
res.Data = YAML.parse(formValues.DataYaml);
}
return res;
}
}
export default KubernetesConfigMapConverter;

View file

@ -0,0 +1,31 @@
import { KubernetesConfiguration, KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
class KubernetesConfigurationConverter {
static secretToConfiguration(secret) {
const res = new KubernetesConfiguration();
res.Type = KubernetesConfigurationTypes.SECRET;
res.Id = secret.Id;
res.Name = secret.Name;
res.Namespace = secret.Namespace;
res.CreationDate = secret.CreationDate;
res.Yaml = secret.Yaml;
res.Data = secret.Data;
res.ConfigurationOwner = secret.ConfigurationOwner;
return res;
}
static configMapToConfiguration(configMap) {
const res = new KubernetesConfiguration();
res.Type = KubernetesConfigurationTypes.CONFIGMAP;
res.Id = configMap.Id;
res.Name = configMap.Name;
res.Namespace = configMap.Namespace;
res.CreationDate = configMap.CreationDate;
res.Yaml = configMap.Yaml;
res.Data = configMap.Data;
res.ConfigurationOwner = configMap.ConfigurationOwner;
return res;
}
}
export default KubernetesConfigurationConverter;

View file

@ -0,0 +1,79 @@
import * as JsonPatch from 'fast-json-patch';
import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models';
import { KubernetesDaemonSetCreatePayload } from 'Kubernetes/models/daemon-set/payloads';
import {
KubernetesPortainerApplicationStackNameLabel,
KubernetesPortainerApplicationNameLabel,
KubernetesPortainerApplicationNote,
KubernetesPortainerApplicationOwnerLabel,
} from 'Kubernetes/models/application/models';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
class KubernetesDaemonSetConverter {
/**
* Generate KubernetesDaemonSet from KubenetesApplicationFormValues
* @param {KubernetesApplicationFormValues} formValues
*/
static applicationFormValuesToDaemonSet(formValues, volumeClaims) {
const res = new KubernetesDaemonSet();
res.Namespace = formValues.ResourcePool.Namespace.Name;
res.Name = formValues.Name;
res.StackName = formValues.StackName ? formValues.StackName : formValues.Name;
res.ApplicationOwner = formValues.ApplicationOwner;
res.ApplicationName = formValues.Name;
res.Image = formValues.Image;
res.CpuLimit = formValues.CpuLimit;
res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit);
res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables);
KubernetesApplicationHelper.generateVolumesFromPersistentVolumClaims(res, volumeClaims);
KubernetesApplicationHelper.generateEnvOrVolumesFromConfigurations(res, formValues.Configurations);
return res;
}
/**
* Generate CREATE payload from DaemonSet
* @param {KubernetesDaemonSetPayload} model DaemonSet to genereate payload from
*/
static createPayload(daemonSet) {
const payload = new KubernetesDaemonSetCreatePayload();
payload.metadata.name = daemonSet.Name;
payload.metadata.namespace = daemonSet.Namespace;
payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = daemonSet.StackName;
payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = daemonSet.ApplicationName;
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = daemonSet.ApplicationOwner;
payload.metadata.annotations[KubernetesPortainerApplicationNote] = daemonSet.Note;
payload.spec.replicas = daemonSet.ReplicaCount;
payload.spec.selector.matchLabels.app = daemonSet.Name;
payload.spec.template.metadata.labels.app = daemonSet.Name;
payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = daemonSet.ApplicationName;
payload.spec.template.spec.containers[0].name = daemonSet.Name;
payload.spec.template.spec.containers[0].image = daemonSet.Image;
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', daemonSet.Env);
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', daemonSet.VolumeMounts);
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.volumes', daemonSet.Volumes);
if (daemonSet.MemoryLimit) {
payload.spec.template.spec.containers[0].resources.limits.memory = daemonSet.MemoryLimit;
payload.spec.template.spec.containers[0].resources.requests.memory = daemonSet.MemoryLimit;
}
if (daemonSet.CpuLimit) {
payload.spec.template.spec.containers[0].resources.limits.cpu = daemonSet.CpuLimit;
payload.spec.template.spec.containers[0].resources.requests.cpu = daemonSet.CpuLimit;
}
if (!daemonSet.CpuLimit && !daemonSet.MemoryLimit) {
delete payload.spec.template.spec.containers[0].resources;
}
return payload;
}
static patchPayload(oldDaemonSet, newDaemonSet) {
const oldPayload = KubernetesDaemonSetConverter.createPayload(oldDaemonSet);
const newPayload = KubernetesDaemonSetConverter.createPayload(newDaemonSet);
const payload = JsonPatch.compare(oldPayload, newPayload);
return payload;
}
}
export default KubernetesDaemonSetConverter;

View file

@ -0,0 +1,80 @@
import * as JsonPatch from 'fast-json-patch';
import { KubernetesDeployment } from 'Kubernetes/models/deployment/models';
import { KubernetesDeploymentCreatePayload } from 'Kubernetes/models/deployment/payloads';
import {
KubernetesPortainerApplicationStackNameLabel,
KubernetesPortainerApplicationNameLabel,
KubernetesPortainerApplicationOwnerLabel,
KubernetesPortainerApplicationNote,
} from 'Kubernetes/models/application/models';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
class KubernetesDeploymentConverter {
/**
* Generate KubernetesDeployment from KubernetesApplicationFormValues
* @param {KubernetesApplicationFormValues} formValues
*/
static applicationFormValuesToDeployment(formValues, volumeClaims) {
const res = new KubernetesDeployment();
res.Namespace = formValues.ResourcePool.Namespace.Name;
res.Name = formValues.Name;
res.StackName = formValues.StackName ? formValues.StackName : formValues.Name;
res.ApplicationOwner = formValues.ApplicationOwner;
res.ApplicationName = formValues.Name;
res.ReplicaCount = formValues.ReplicaCount;
res.Image = formValues.Image;
res.CpuLimit = formValues.CpuLimit;
res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit);
res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables);
KubernetesApplicationHelper.generateVolumesFromPersistentVolumClaims(res, volumeClaims);
KubernetesApplicationHelper.generateEnvOrVolumesFromConfigurations(res, formValues.Configurations);
return res;
}
/**
* Generate CREATE payload from Deployment
* @param {KubernetesDeploymentPayload} model Deployment to genereate payload from
*/
static createPayload(deployment) {
const payload = new KubernetesDeploymentCreatePayload();
payload.metadata.name = deployment.Name;
payload.metadata.namespace = deployment.Namespace;
payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = deployment.StackName;
payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = deployment.ApplicationName;
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = deployment.ApplicationOwner;
payload.metadata.annotations[KubernetesPortainerApplicationNote] = deployment.Note;
payload.spec.replicas = deployment.ReplicaCount;
payload.spec.selector.matchLabels.app = deployment.Name;
payload.spec.template.metadata.labels.app = deployment.Name;
payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = deployment.ApplicationName;
payload.spec.template.spec.containers[0].name = deployment.Name;
payload.spec.template.spec.containers[0].image = deployment.Image;
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', deployment.Env);
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', deployment.VolumeMounts);
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.volumes', deployment.Volumes);
if (deployment.MemoryLimit) {
payload.spec.template.spec.containers[0].resources.limits.memory = deployment.MemoryLimit;
payload.spec.template.spec.containers[0].resources.requests.memory = deployment.MemoryLimit;
}
if (deployment.CpuLimit) {
payload.spec.template.spec.containers[0].resources.limits.cpu = deployment.CpuLimit;
payload.spec.template.spec.containers[0].resources.requests.cpu = deployment.CpuLimit;
}
if (!deployment.CpuLimit && !deployment.MemoryLimit) {
delete payload.spec.template.spec.containers[0].resources;
}
return payload;
}
static patchPayload(oldDeployment, newDeployment) {
const oldPayload = KubernetesDeploymentConverter.createPayload(oldDeployment);
const newPayload = KubernetesDeploymentConverter.createPayload(newDeployment);
const payload = JsonPatch.compare(oldPayload, newPayload);
return payload;
}
}
export default KubernetesDeploymentConverter;

View file

@ -0,0 +1,15 @@
import { KubernetesEvent } from 'Kubernetes/models/event/models';
class KubernetesEventConverter {
static apiToEvent(data) {
const res = new KubernetesEvent();
res.Id = data.metadata.uid;
res.Date = data.lastTimestamp || data.eventTime;
res.Type = data.type;
res.Message = data.message;
res.Involved = data.involvedObject;
return res;
}
}
export default KubernetesEventConverter;

View file

@ -0,0 +1,29 @@
import { KubernetesNamespace } from 'Kubernetes/models/namespace/models';
import { KubernetesNamespaceCreatePayload } from 'Kubernetes/models/namespace/payloads';
import { KubernetesPortainerResourcePoolNameLabel, KubernetesPortainerResourcePoolOwnerLabel } from 'Kubernetes/models/resource-pool/models';
class KubernetesNamespaceConverter {
static apiToNamespace(data, yaml) {
const res = new KubernetesNamespace();
res.Id = data.metadata.uid;
res.Name = data.metadata.name;
res.CreationDate = data.metadata.creationTimestamp;
res.Status = data.status.phase;
res.Yaml = yaml ? yaml.data : '';
res.ResourcePoolName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerResourcePoolNameLabel] : '';
res.ResourcePoolOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] : '';
return res;
}
static createPayload(namespace) {
const res = new KubernetesNamespaceCreatePayload();
res.metadata.name = namespace.Name;
res.metadata.labels[KubernetesPortainerResourcePoolNameLabel] = namespace.ResourcePoolName;
if (namespace.ResourcePoolOwner) {
res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = namespace.ResourcePoolOwner;
}
return res;
}
}
export default KubernetesNamespaceConverter;

View file

@ -0,0 +1,65 @@
import _ from 'lodash-es';
import { KubernetesNode, KubernetesNodeDetails } from 'Kubernetes/models/node/models';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
class KubernetesNodeConverter {
static apiToNode(data, res) {
if (!res) {
res = new KubernetesNode();
}
res.Id = data.metadata.uid;
const hostName = _.find(data.status.addresses, { type: 'Hostname' });
res.Name = hostName ? hostName.address : data.metadata.Name;
res.Role = _.has(data.metadata.labels, 'node-role.kubernetes.io/master') ? 'Manager' : 'Worker';
const ready = _.find(data.status.conditions, { type: KubernetesNodeConditionTypes.READY });
const memoryPressure = _.find(data.status.conditions, { type: KubernetesNodeConditionTypes.MEMORY_PRESSURE });
const PIDPressure = _.find(data.status.conditions, { type: KubernetesNodeConditionTypes.PID_PRESSURE });
const diskPressure = _.find(data.status.conditions, { type: KubernetesNodeConditionTypes.DISK_PRESSURE });
const networkUnavailable = _.find(data.status.conditions, { type: KubernetesNodeConditionTypes.NETWORK_UNAVAILABLE });
res.Conditions = {
MemoryPressure: memoryPressure && memoryPressure.status === 'True',
PIDPressure: PIDPressure && PIDPressure.status === 'True',
DiskPressure: diskPressure && diskPressure.status === 'True',
NetworkUnavailable: networkUnavailable && networkUnavailable.status === 'True',
};
if (ready.status === 'False') {
res.Status = 'Unhealthy';
} else if (ready.status === 'Unknown' || res.Conditions.MemoryPressure || res.Conditions.PIDPressure || res.Conditions.DiskPressure || res.Conditions.NetworkUnavailable) {
res.Status = 'Warning';
} else {
res.Status = 'Ready';
}
res.CPU = KubernetesResourceReservationHelper.parseCPU(data.status.allocatable.cpu);
res.Memory = data.status.allocatable.memory;
res.Version = data.status.nodeInfo.kubeletVersion;
const internalIP = _.find(data.status.addresses, { type: 'InternalIP' });
res.IPAddress = internalIP ? internalIP.address : '-';
return res;
}
static apiToNodeDetails(data, yaml) {
let res = new KubernetesNodeDetails();
res = KubernetesNodeConverter.apiToNode(data, res);
res.CreationDate = data.metadata.creationTimestamp;
res.OS.Architecture = data.status.nodeInfo.architecture;
res.OS.Platform = data.status.nodeInfo.operatingSystem;
res.OS.Image = data.status.nodeInfo.osImage;
res.Yaml = yaml ? yaml.data : '';
return res;
}
}
export const KubernetesNodeConditionTypes = Object.freeze({
READY: 'Ready',
MEMORY_PRESSURE: 'MemoryPressure',
PID_PRESSURE: 'PIDPressure',
DISK_PRESSURE: 'DiskPressure',
NETWORK_UNAVAILABLE: 'NetworkUnavailable',
});
export default KubernetesNodeConverter;

View file

@ -0,0 +1,68 @@
import _ from 'lodash-es';
import * as JsonPatch from 'fast-json-patch';
import { KubernetesPersistentVolumeClaim } from 'Kubernetes/models/volume/models';
import { KubernetesPersistentVolumClaimCreatePayload } from 'Kubernetes/models/volume/payloads';
import { KubernetesPortainerApplicationOwnerLabel, KubernetesPortainerApplicationNameLabel } from 'Kubernetes/models/application/models';
class KubernetesPersistentVolumeClaimConverter {
static apiToPersistentVolumeClaim(data, storageClasses, yaml) {
const res = new KubernetesPersistentVolumeClaim();
res.Id = data.metadata.uid;
res.Name = data.metadata.name;
res.Namespace = data.metadata.namespace;
res.CreationDate = data.metadata.creationTimestamp;
res.Storage = data.spec.resources.requests.storage.replace('i', '') + 'B';
res.StorageClass = _.find(storageClasses, { Name: data.spec.storageClassName });
res.Yaml = yaml ? yaml.data : '';
res.ApplicationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationOwnerLabel] : '';
res.ApplicationName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationNameLabel] : '';
return res;
}
/**
* Generate KubernetesPersistentVolumeClaim list from KubernetesApplicationFormValues
* @param {KubernetesApplicationFormValues} formValues
*/
static applicationFormValuesToVolumeClaims(formValues) {
_.remove(formValues.PersistedFolders, (item) => item.NeedsDeletion);
const res = _.map(formValues.PersistedFolders, (item) => {
const pvc = new KubernetesPersistentVolumeClaim();
if (item.PersistentVolumeClaimName) {
pvc.Name = item.PersistentVolumeClaimName;
pvc.PreviousName = item.PersistentVolumeClaimName;
} else {
pvc.Name = formValues.Name + '-' + pvc.Name;
}
pvc.MountPath = item.ContainerPath;
pvc.Namespace = formValues.ResourcePool.Namespace.Name;
pvc.Storage = '' + item.Size + item.SizeUnit.charAt(0) + 'i';
pvc.StorageClass = item.StorageClass;
pvc.ApplicationOwner = formValues.ApplicationOwner;
pvc.ApplicationName = formValues.Name;
return pvc;
});
return res;
}
static createPayload(pvc) {
const res = new KubernetesPersistentVolumClaimCreatePayload();
res.metadata.name = pvc.Name;
res.metadata.namespace = pvc.Namespace;
res.spec.resources.requests.storage = pvc.Storage;
res.spec.storageClassName = pvc.StorageClass.Name;
res.metadata.labels.app = pvc.ApplicationName;
res.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = pvc.ApplicationOwner;
res.metadata.labels[KubernetesPortainerApplicationNameLabel] = pvc.ApplicationName;
return res;
}
static patchPayload(oldPVC, newPVC) {
const oldPayload = KubernetesPersistentVolumeClaimConverter.createPayload(oldPVC);
const newPayload = KubernetesPersistentVolumeClaimConverter.createPayload(newPVC);
const payload = JsonPatch.compare(oldPayload, newPayload);
return payload;
}
}
export default KubernetesPersistentVolumeClaimConverter;

View file

@ -0,0 +1,32 @@
import _ from 'lodash-es';
import { KubernetesPod } from 'Kubernetes/models/pod/models';
class KubernetesPodConverter {
static computeStatus(statuses) {
const containerStatuses = _.map(statuses, 'state');
const running = _.filter(containerStatuses, (s) => s.running).length;
const waiting = _.filter(containerStatuses, (s) => s.waiting).length;
if (waiting) {
return 'Waiting';
} else if (!running) {
return 'Terminated';
}
return 'Running';
}
static apiToPod(data) {
const res = new KubernetesPod();
res.Id = data.metadata.uid;
res.Name = data.metadata.name;
res.Namespace = data.metadata.namespace;
res.Images = _.map(data.spec.containers, 'image');
res.Status = KubernetesPodConverter.computeStatus(data.status.containerStatuses);
res.Restarts = _.sumBy(data.status.containerStatuses, 'restartCount');
res.Node = data.spec.nodeName;
res.CreationDate = data.status.startTime;
res.Containers = data.spec.containers;
res.Labels = data.metadata.labels;
return res;
}
}
export default KubernetesPodConverter;

View file

@ -0,0 +1,12 @@
import { KubernetesResourcePool } from 'Kubernetes/models/resource-pool/models';
class KubernetesResourcePoolConverter {
static apiToResourcePool(namespace) {
const res = new KubernetesResourcePool();
res.Namespace = namespace;
res.Yaml = namespace.Yaml;
return res;
}
}
export default KubernetesResourcePoolConverter;

View file

@ -0,0 +1,87 @@
import filesizeParser from 'filesize-parser';
import { KubernetesResourceQuota } from 'Kubernetes/models/resource-quota/models';
import { KubernetesResourceQuotaCreatePayload, KubernetesResourceQuotaUpdatePayload } from 'Kubernetes/models/resource-quota/payloads';
import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper';
import { KubernetesPortainerResourcePoolNameLabel, KubernetesPortainerResourcePoolOwnerLabel } from 'Kubernetes/models/resource-pool/models';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
class KubernetesResourceQuotaConverter {
static apiToResourceQuota(data, yaml) {
const res = new KubernetesResourceQuota();
res.Id = data.metadata.uid;
res.Namespace = data.metadata.namespace;
res.Name = data.metadata.name;
res.CpuLimit = 0;
res.MemoryLimit = 0;
if (data.spec.hard && data.spec.hard['limits.cpu']) {
res.CpuLimit = KubernetesResourceReservationHelper.parseCPU(data.spec.hard['limits.cpu']);
}
if (data.spec.hard && data.spec.hard['limits.memory']) {
res.MemoryLimit = filesizeParser(data.spec.hard['limits.memory'], { base: 10 });
}
res.MemoryLimitUsed = 0;
if (data.status.used && data.status.used['limits.memory']) {
res.MemoryLimitUsed = filesizeParser(data.status.used['limits.memory'], { base: 10 });
}
res.CpuLimitUsed = 0;
if (data.status.used && data.status.used['limits.cpu']) {
res.CpuLimitUsed = KubernetesResourceReservationHelper.parseCPU(data.status.used['limits.cpu']);
}
res.Yaml = yaml ? yaml.data : '';
res.ResourcePoolName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerResourcePoolNameLabel] : '';
res.ResourcePoolOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] : '';
return res;
}
static createPayload(quota) {
const res = new KubernetesResourceQuotaCreatePayload();
res.metadata.name = KubernetesResourceQuotaHelper.generateResourceQuotaName(quota.Namespace);
res.metadata.namespace = quota.Namespace;
res.spec.hard['requests.cpu'] = quota.CpuLimit;
res.spec.hard['requests.memory'] = quota.MemoryLimit;
res.spec.hard['limits.cpu'] = quota.CpuLimit;
res.spec.hard['limits.memory'] = quota.MemoryLimit;
res.metadata.labels[KubernetesPortainerResourcePoolNameLabel] = quota.ResourcePoolName;
if (quota.ResourcePoolOwner) {
res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = quota.ResourcePoolOwner;
}
if (!quota.CpuLimit || quota.CpuLimit === 0) {
delete res.spec.hard['requests.cpu'];
delete res.spec.hard['limits.cpu'];
}
if (!quota.MemoryLimit || quota.MemoryLimit === 0) {
delete res.spec.hard['requests.memory'];
delete res.spec.hard['limits.memory'];
}
return res;
}
static updatePayload(quota) {
const res = new KubernetesResourceQuotaUpdatePayload();
res.metadata.name = quota.Name;
res.metadata.namespace = quota.Namespace;
res.metadata.uid = quota.Id;
res.spec.hard['requests.cpu'] = quota.CpuLimit;
res.spec.hard['requests.memory'] = quota.MemoryLimit;
res.spec.hard['limits.cpu'] = quota.CpuLimit;
res.spec.hard['limits.memory'] = quota.MemoryLimit;
res.metadata.labels[KubernetesPortainerResourcePoolNameLabel] = quota.ResourcePoolName;
if (quota.ResourcePoolOwner) {
res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = quota.ResourcePoolOwner;
}
if (!quota.CpuLimit || quota.CpuLimit === 0) {
delete res.spec.hard['requests.cpu'];
delete res.spec.hard['limits.cpu'];
}
if (!quota.MemoryLimit || quota.MemoryLimit === 0) {
delete res.spec.hard['requests.memory'];
delete res.spec.hard['limits.memory'];
}
return res;
}
}
export default KubernetesResourceQuotaConverter;

View file

@ -0,0 +1,58 @@
import { KubernetesSecretCreatePayload, KubernetesSecretUpdatePayload } from 'Kubernetes/models/secret/payloads';
import { KubernetesApplicationSecret } from 'Kubernetes/models/secret/models';
import YAML from 'yaml';
import _ from 'lodash-es';
import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models';
class KubernetesSecretConverter {
static createPayload(secret) {
const res = new KubernetesSecretCreatePayload();
res.metadata.name = secret.Name;
res.metadata.namespace = secret.Namespace;
res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = secret.ConfigurationOwner;
res.stringData = secret.Data;
return res;
}
static updatePayload(secret) {
const res = new KubernetesSecretUpdatePayload();
res.metadata.name = secret.Name;
res.metadata.namespace = secret.Namespace;
res.stringData = secret.Data;
return res;
}
static apiToSecret(payload, yaml) {
const res = new KubernetesApplicationSecret();
res.Id = payload.metadata.uid;
res.Name = payload.metadata.name;
res.Namespace = payload.metadata.namespace;
res.ConfigurationOwner = payload.metadata.labels ? payload.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : '';
res.CreationDate = payload.metadata.creationTimestamp;
res.Yaml = yaml ? yaml.data : '';
res.Data = payload.data;
return res;
}
static configurationFormValuesToSecret(formValues) {
const res = new KubernetesApplicationSecret();
res.Name = formValues.Name;
res.Namespace = formValues.ResourcePool.Namespace.Name;
res.ConfigurationOwner = formValues.ConfigurationOwner;
if (formValues.IsSimple) {
res.Data = _.reduce(
formValues.Data,
(acc, entry) => {
acc[entry.Key] = entry.Value;
return acc;
},
{}
);
} else {
res.Data = YAML.parse(formValues.DataYaml);
}
return res;
}
}
export default KubernetesSecretConverter;

View file

@ -0,0 +1,88 @@
import _ from 'lodash-es';
import * as JsonPatch from 'fast-json-patch';
import { KubernetesServiceCreatePayload } from 'Kubernetes/models/service/payloads';
import {
KubernetesPortainerApplicationStackNameLabel,
KubernetesPortainerApplicationNameLabel,
KubernetesPortainerApplicationOwnerLabel,
} from 'Kubernetes/models/application/models';
import { KubernetesServiceHeadlessClusterIP, KubernetesService, KubernetesServicePort, KubernetesServiceTypes } from 'Kubernetes/models/service/models';
import { KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models';
import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper';
class KubernetesServiceConverter {
static publishedPortToServicePort(name, publishedPort, type) {
const res = new KubernetesServicePort();
res.name = _.toLower(name + '-' + publishedPort.ContainerPort + '-' + publishedPort.Protocol);
res.port = type === KubernetesServiceTypes.LOAD_BALANCER ? publishedPort.LoadBalancerPort : publishedPort.ContainerPort;
res.targetPort = publishedPort.ContainerPort;
res.protocol = publishedPort.Protocol;
if (type === KubernetesServiceTypes.NODE_PORT && publishedPort.NodePort) {
res.nodePort = publishedPort.NodePort;
} else if (type === KubernetesServiceTypes.LOAD_BALANCER && publishedPort.LoadBalancerNodePort) {
res.nodePort = publishedPort.LoadBalancerNodePort;
} else {
delete res.nodePort;
}
return res;
}
/**
* Generate KubernetesService from KubernetesApplicationFormValues
* @param {KubernetesApplicationFormValues} formValues
*/
static applicationFormValuesToService(formValues) {
const res = new KubernetesService();
res.Namespace = formValues.ResourcePool.Namespace.Name;
res.Name = formValues.Name;
res.StackName = formValues.StackName ? formValues.StackName : formValues.Name;
res.ApplicationOwner = formValues.ApplicationOwner;
res.ApplicationName = formValues.Name;
if (formValues.PublishingType === KubernetesApplicationPublishingTypes.CLUSTER) {
res.Type = KubernetesServiceTypes.NODE_PORT;
} else if (formValues.PublishingType === KubernetesApplicationPublishingTypes.LOAD_BALANCER) {
res.Type = KubernetesServiceTypes.LOAD_BALANCER;
}
res.Ports = _.map(formValues.PublishedPorts, (item) => KubernetesServiceConverter.publishedPortToServicePort(formValues.Name, item, res.Type));
return res;
}
static applicationFormValuesToHeadlessService(formValues) {
const res = KubernetesServiceConverter.applicationFormValuesToService(formValues);
res.Name = KubernetesServiceHelper.generateHeadlessServiceName(formValues.Name);
res.Headless = true;
return res;
}
/**
* Generate CREATE payload from Service
* @param {KubernetesService} model Service to genereate payload from
*/
static createPayload(service) {
const payload = new KubernetesServiceCreatePayload();
payload.metadata.name = service.Name;
payload.metadata.namespace = service.Namespace;
payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = service.StackName;
payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = service.ApplicationName;
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = service.ApplicationOwner;
payload.spec.ports = service.Ports;
payload.spec.selector.app = service.ApplicationName;
if (service.Headless) {
payload.spec.clusterIP = KubernetesServiceHeadlessClusterIP;
delete payload.spec.ports;
} else if (service.Type) {
payload.spec.type = service.Type;
}
return payload;
}
static patchPayload(oldService, newService) {
const oldPayload = KubernetesServiceConverter.createPayload(oldService);
const newPayload = KubernetesServiceConverter.createPayload(newService);
const payload = JsonPatch.compare(oldPayload, newPayload);
return payload;
}
}
export default KubernetesServiceConverter;

View file

@ -0,0 +1,84 @@
import _ from 'lodash-es';
import * as JsonPatch from 'fast-json-patch';
import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models';
import { KubernetesStatefulSetCreatePayload } from 'Kubernetes/models/stateful-set/payloads';
import {
KubernetesPortainerApplicationStackNameLabel,
KubernetesPortainerApplicationNameLabel,
KubernetesPortainerApplicationOwnerLabel,
KubernetesPortainerApplicationNote,
} from 'Kubernetes/models/application/models';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
import KubernetesPersistentVolumeClaimConverter from './persistentVolumeClaim';
class KubernetesStatefulSetConverter {
/**
* Generate KubernetesStatefulSet from KubernetesApplicationFormValues
* @param {KubernetesApplicationFormValues} formValues
*/
static applicationFormValuesToStatefulSet(formValues, volumeClaims) {
const res = new KubernetesStatefulSet();
res.Namespace = formValues.ResourcePool.Namespace.Name;
res.Name = formValues.Name;
res.StackName = formValues.StackName ? formValues.StackName : formValues.Name;
res.ApplicationOwner = formValues.ApplicationOwner;
res.ApplicationName = formValues.Name;
res.ReplicaCount = formValues.ReplicaCount;
res.Image = formValues.Image;
res.CpuLimit = formValues.CpuLimit;
res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit);
res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables);
KubernetesApplicationHelper.generateVolumesFromPersistentVolumClaims(res, volumeClaims);
KubernetesApplicationHelper.generateEnvOrVolumesFromConfigurations(res, formValues.Configurations);
return res;
}
/**
* Generate CREATE payload from StatefulSet
* @param {KubernetesStatefulSetPayload} model StatefulSet to genereate payload from
*/
static createPayload(statefulSet) {
const payload = new KubernetesStatefulSetCreatePayload();
payload.metadata.name = statefulSet.Name;
payload.metadata.namespace = statefulSet.Namespace;
payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = statefulSet.StackName;
payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = statefulSet.ApplicationName;
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = statefulSet.ApplicationOwner;
payload.metadata.annotations[KubernetesPortainerApplicationNote] = statefulSet.Note;
payload.spec.replicas = statefulSet.ReplicaCount;
payload.spec.serviceName = statefulSet.ServiceName;
payload.spec.selector.matchLabels.app = statefulSet.Name;
payload.spec.volumeClaimTemplates = _.map(statefulSet.VolumeClaims, (item) => KubernetesPersistentVolumeClaimConverter.createPayload(item));
payload.spec.template.metadata.labels.app = statefulSet.Name;
payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = statefulSet.ApplicationName;
payload.spec.template.spec.containers[0].name = statefulSet.Name;
payload.spec.template.spec.containers[0].image = statefulSet.Image;
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', statefulSet.Env);
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', statefulSet.VolumeMounts);
KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.volumes', statefulSet.Volumes);
if (statefulSet.MemoryLimit) {
payload.spec.template.spec.containers[0].resources.limits.memory = statefulSet.MemoryLimit;
payload.spec.template.spec.containers[0].resources.requests.memory = statefulSet.MemoryLimit;
}
if (statefulSet.CpuLimit) {
payload.spec.template.spec.containers[0].resources.limits.cpu = statefulSet.CpuLimit;
payload.spec.template.spec.containers[0].resources.requests.cpu = statefulSet.CpuLimit;
}
if (!statefulSet.CpuLimit && !statefulSet.MemoryLimit) {
delete payload.spec.template.spec.containers[0].resources;
}
return payload;
}
static patchPayload(oldSFS, newSFS) {
const oldPayload = KubernetesStatefulSetConverter.createPayload(oldSFS);
const newPayload = KubernetesStatefulSetConverter.createPayload(newSFS);
const payload = JsonPatch.compare(oldPayload, newPayload);
return payload;
}
}
export default KubernetesStatefulSetConverter;

View file

@ -0,0 +1,14 @@
import { KubernetesStorageClass } from 'Kubernetes/models/storage-class/models';
class KubernetesStorageClassConverter {
/**
* API StorageClass to front StorageClass
*/
static apiToStorageClass(data) {
const res = new KubernetesStorageClass();
res.Name = data.metadata.name;
return res;
}
}
export default KubernetesStorageClassConverter;

View file

@ -0,0 +1,12 @@
import { KubernetesVolume } from 'Kubernetes/models/volume/models';
class KubernetesVolumeConverter {
static pvcToVolume(claim, pool) {
const res = new KubernetesVolume();
res.PersistentVolumeClaim = claim;
res.ResourcePool = pool;
return res;
}
}
export default KubernetesVolumeConverter;

View file

@ -0,0 +1,72 @@
import _ from 'lodash-es';
import { KubernetesApplicationDataAccessPolicies } from 'Kubernetes/models/application/models';
angular
.module('portainer.kubernetes')
.filter('kubernetesApplicationServiceTypeIcon', function () {
'use strict';
return function (text) {
var status = _.toLower(text);
switch (status) {
case 'loadbalancer':
return 'fa-project-diagram';
case 'clusterip':
return 'fa-list-alt';
case 'nodeport':
return 'fa-list';
}
};
})
.filter('kubernetesApplicationServiceTypeText', function () {
'use strict';
return function (text) {
var status = _.toLower(text);
switch (status) {
case 'loadbalancer':
return 'Load balancer';
case 'clusterip':
return 'Internal';
case 'nodeport':
return 'Cluster';
}
};
})
.filter('kubernetesApplicationCPUValue', function () {
'use strict';
return function (value) {
return _.round(value, 2);
};
})
.filter('kubernetesApplicationDataAccessPolicyIcon', function () {
'use strict';
return function (value) {
switch (value) {
case KubernetesApplicationDataAccessPolicies.ISOLATED:
return 'fa-cubes';
case KubernetesApplicationDataAccessPolicies.SHARED:
return 'fa-cube';
}
};
})
.filter('kubernetesApplicationDataAccessPolicyText', function () {
'use strict';
return function (value) {
switch (value) {
case KubernetesApplicationDataAccessPolicies.ISOLATED:
return 'Isolated';
case KubernetesApplicationDataAccessPolicies.SHARED:
return 'Shared';
}
};
})
.filter('kubernetesApplicationDataAccessPolicyTooltip', function () {
'use strict';
return function (value) {
switch (value) {
case KubernetesApplicationDataAccessPolicies.ISOLATED:
return 'All the instances of this application are using their own data.';
case KubernetesApplicationDataAccessPolicies.SHARED:
return 'All the instances of this application are sharing the same data.';
}
};
});

View file

@ -0,0 +1,13 @@
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
angular.module('portainer.kubernetes').filter('kubernetesConfigurationTypeText', function () {
'use strict';
return function (type) {
switch (type) {
case KubernetesConfigurationTypes.SECRET:
return 'Sensitive';
case KubernetesConfigurationTypes.CONFIGMAP:
return 'Non-sensitive';
}
};
});

View file

@ -0,0 +1,16 @@
import _ from 'lodash-es';
angular.module('portainer.kubernetes').filter('kubernetesEventTypeColor', function () {
'use strict';
return function (text) {
var status = _.toLower(text);
switch (status) {
case 'normal':
return 'info';
case 'warning':
return 'warning';
default:
return 'danger';
}
};
});

View file

@ -0,0 +1,11 @@
angular.module('portainer.kubernetes').filter('kubernetesUsageLevelInfo', function () {
return function (usage) {
if (usage >= 80) {
return 'danger';
} else if (usage > 50 && usage < 80) {
return 'warning';
} else {
return 'success';
}
};
});

View file

@ -0,0 +1,35 @@
import _ from 'lodash-es';
angular
.module('portainer.kubernetes')
.filter('kubernetesNodeStatusColor', function () {
'use strict';
return function (text) {
var status = _.toLower(text);
switch (status) {
case 'ready':
return 'success';
case 'warning':
return 'warning';
default:
return 'danger';
}
};
})
.filter('kubernetesNodeConditionsMessage', function () {
'use strict';
return function (conditions) {
if (conditions.MemoryPressure) {
return 'Node memory is running low';
}
if (conditions.PIDPressure) {
return 'Too many processes running on the node';
}
if (conditions.DiskPressure) {
return 'Node disk capacity is running low';
}
if (conditions.NetworkUnavailable) {
return 'Incorrect node network configuration';
}
};
});

View file

@ -0,0 +1,84 @@
import _ from 'lodash-es';
angular
.module('portainer.kubernetes')
.filter('kubernetesPodStatusColor', function () {
'use strict';
return function (text) {
var status = _.toLower(text);
switch (status) {
case 'running':
return 'success';
case 'waiting':
return 'warning';
case 'terminated':
return 'info';
default:
return 'danger';
}
};
})
.filter('kubernetesPodConditionStatusBadge', function () {
'use strict';
return function (status, type) {
switch (type) {
case 'Unschedulable':
switch (status) {
case 'True':
return 'fa-exclamation-triangle red-icon';
case 'False':
return 'fa-check green-icon';
case 'Unknown':
return 'fa-exclamation-circle orange-icon';
}
break;
case 'PodScheduled':
case 'Ready':
case 'Initialized':
case 'ContainersReady':
switch (status) {
case 'True':
return 'fa-check green-icon';
case 'False':
return 'fa-exclamation-triangle red-icon';
case 'Unknown':
return 'fa-exclamation-circle orange-icon';
}
break;
default:
return 'fa-question-circle red-icon';
}
};
})
.filter('kubernetesPodConditionStatusText', function () {
'use strict';
return function (status, type) {
switch (type) {
case 'Unschedulable':
switch (status) {
case 'True':
return 'Alert';
case 'False':
return 'OK';
case 'Unknown':
return 'Warning';
}
break;
case 'PodScheduled':
case 'Ready':
case 'Initialized':
case 'ContainersReady':
switch (status) {
case 'True':
return 'Ok';
case 'False':
return 'Alert';
case 'Unknown':
return 'Warning';
}
break;
default:
return 'Unknown';
}
};
});

View file

@ -0,0 +1,273 @@
import _ from 'lodash-es';
import { KubernetesPortMapping, KubernetesPortMappingPort } from 'Kubernetes/models/port/models';
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import {
KubernetesApplicationConfigurationFormValueOverridenKeyTypes,
KubernetesApplicationEnvironmentVariableFormValue,
KubernetesApplicationConfigurationFormValue,
KubernetesApplicationConfigurationFormValueOverridenKey,
KubernetesApplicationPersistedFolderFormValue,
KubernetesApplicationPublishedPortFormValue,
} from 'Kubernetes/models/application/formValues';
import {
KubernetesApplicationEnvConfigMapPayload,
KubernetesApplicationEnvPayload,
KubernetesApplicationEnvSecretPayload,
KubernetesApplicationVolumeConfigMapPayload,
KubernetesApplicationVolumeEntryPayload,
KubernetesApplicationVolumeMountPayload,
KubernetesApplicationVolumePersistentPayload,
KubernetesApplicationVolumeSecretPayload,
} from 'Kubernetes/models/application/payloads';
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
class KubernetesApplicationHelper {
static associatePodsAndApplication(pods, app) {
return _.filter(pods, { Labels: app.spec.selector.matchLabels });
}
static portMappingsFromApplications(applications) {
const res = _.reduce(
applications,
(acc, app) => {
if (app.PublishedPorts.length > 0) {
const mapping = new KubernetesPortMapping();
mapping.Name = app.Name;
mapping.ResourcePool = app.ResourcePool;
mapping.ServiceType = app.ServiceType;
mapping.LoadBalancerIPAddress = app.LoadBalancerIPAddress;
mapping.ApplicationOwner = app.ApplicationOwner;
mapping.Ports = _.map(app.PublishedPorts, (item) => {
const port = new KubernetesPortMappingPort();
port.Port = mapping.ServiceType === KubernetesServiceTypes.NODE_PORT ? item.nodePort : item.port;
port.TargetPort = item.targetPort;
port.Protocol = item.protocol;
return port;
});
acc.push(mapping);
}
return acc;
},
[]
);
return res;
}
/**
* FORMVALUES TO APPLICATION FUNCTIONS
*/
static generateEnvFromEnvVariables(envVariables) {
_.remove(envVariables, (item) => item.NeedsDeletion);
const env = _.map(envVariables, (item) => {
const res = new KubernetesApplicationEnvPayload();
res.name = item.Name;
res.value = item.Value;
return res;
});
return env;
}
static generateEnvOrVolumesFromConfigurations(app, configurations) {
let finalEnv = [];
let finalVolumes = [];
let finalMounts = [];
_.forEach(configurations, (config) => {
const isBasic = config.SelectedConfiguration.Type === KubernetesConfigurationTypes.CONFIGMAP;
if (!config.Overriden) {
const envKeys = _.keys(config.SelectedConfiguration.Data);
_.forEach(envKeys, (item) => {
const res = isBasic ? new KubernetesApplicationEnvConfigMapPayload() : new KubernetesApplicationEnvSecretPayload();
res.name = item;
if (isBasic) {
res.valueFrom.configMapKeyRef.name = config.SelectedConfiguration.Name;
res.valueFrom.configMapKeyRef.key = item;
} else {
res.valueFrom.secretKeyRef.name = config.SelectedConfiguration.Name;
res.valueFrom.secretKeyRef.key = item;
}
finalEnv.push(res);
});
} else {
const envKeys = _.filter(config.OverridenKeys, (item) => item.Type === KubernetesApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT);
_.forEach(envKeys, (item) => {
const res = isBasic ? new KubernetesApplicationEnvConfigMapPayload() : new KubernetesApplicationEnvSecretPayload();
res.name = item.Key;
if (isBasic) {
res.valueFrom.configMapKeyRef.name = config.SelectedConfiguration.Name;
res.valueFrom.configMapKeyRef.key = item.Key;
} else {
res.valueFrom.secretKeyRef.name = config.SelectedConfiguration.Name;
res.valueFrom.secretKeyRef.key = item.Key;
}
finalEnv.push(res);
});
const volKeys = _.filter(config.OverridenKeys, (item) => item.Type === KubernetesApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM);
const groupedVolKeys = _.groupBy(volKeys, 'Path');
_.forEach(groupedVolKeys, (items, path) => {
const volumeName = KubernetesVolumeHelper.generatedApplicationConfigVolumeName(app.Name);
const configurationName = config.SelectedConfiguration.Name;
const itemsMap = _.map(items, (item) => {
const entry = new KubernetesApplicationVolumeEntryPayload();
entry.key = item.Key;
entry.path = item.Key;
return entry;
});
const mount = isBasic ? new KubernetesApplicationVolumeMountPayload() : new KubernetesApplicationVolumeMountPayload(true);
const volume = isBasic ? new KubernetesApplicationVolumeConfigMapPayload() : new KubernetesApplicationVolumeSecretPayload();
mount.name = volumeName;
mount.mountPath = path;
volume.name = volumeName;
if (isBasic) {
volume.configMap.name = configurationName;
volume.configMap.items = itemsMap;
} else {
volume.secret.secretName = configurationName;
volume.secret.items = itemsMap;
}
finalMounts.push(mount);
finalVolumes.push(volume);
});
}
});
app.Env = _.concat(app.Env, finalEnv);
app.Volumes = _.concat(app.Volumes, finalVolumes);
app.VolumeMounts = _.concat(app.VolumeMounts, finalMounts);
return app;
}
static generateVolumesFromPersistentVolumClaims(app, volumeClaims) {
app.VolumeMounts = [];
app.Volumes = [];
_.forEach(volumeClaims, (item) => {
const volumeMount = new KubernetesApplicationVolumeMountPayload();
const name = item.Name;
volumeMount.name = name;
volumeMount.mountPath = item.MountPath;
app.VolumeMounts.push(volumeMount);
const volume = new KubernetesApplicationVolumePersistentPayload();
volume.name = name;
volume.persistentVolumeClaim.claimName = name;
app.Volumes.push(volume);
});
}
/**
* !FORMVALUES TO APPLICATION FUNCTIONS
*/
/**
* APPLICATION TO FORMVALUES FUNCTIONS
*/
static generateEnvVariablesFromEnv(env) {
const envVariables = _.map(env, (item) => {
if (!item.value) {
return;
}
const res = new KubernetesApplicationEnvironmentVariableFormValue();
res.Name = item.name;
res.Value = item.value;
res.IsNew = false;
return res;
});
return _.without(envVariables, undefined);
}
static generateConfigurationFormValuesFromEnvAndVolumes(env, volumes, configurations) {
const finalRes = _.flatMap(configurations, (cfg) => {
const filterCondition = cfg.Type === KubernetesConfigurationTypes.CONFIGMAP ? 'valueFrom.configMapKeyRef.name' : 'valueFrom.secretKeyRef.name';
const cfgEnv = _.filter(env, [filterCondition, cfg.Name]);
const cfgVol = _.filter(volumes, { configurationName: cfg.Name });
if (!cfgEnv.length && !cfgVol.length) {
return;
}
const keys = _.reduce(
_.keys(cfg.Data),
(acc, k) => {
const keyEnv = _.filter(cfgEnv, { name: k });
const keyVol = _.filter(cfgVol, { configurationKey: k });
const key = {
Key: k,
Count: keyEnv.length + keyVol.length,
Sum: _.concat(keyEnv, keyVol),
EnvCount: keyEnv.length,
VolCount: keyVol.length,
};
acc.push(key);
return acc;
},
[]
);
const max = _.max(_.map(keys, 'Count'));
const overrideThreshold = max - _.max(_.map(keys, 'VolCount'));
const res = _.map(new Array(max), () => new KubernetesApplicationConfigurationFormValue());
_.forEach(res, (item, index) => {
item.SelectedConfiguration = cfg;
const overriden = index >= overrideThreshold;
if (overriden) {
item.Overriden = true;
item.OverridenKeys = _.map(keys, (k) => {
const fvKey = new KubernetesApplicationConfigurationFormValueOverridenKey();
fvKey.Key = k.Key;
if (index < k.EnvCount) {
fvKey.Type = KubernetesApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT;
} else {
fvKey.Type = KubernetesApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM;
fvKey.Path = k.Sum[index].rootMountPath;
}
return fvKey;
});
}
});
return res;
});
return _.without(finalRes, undefined);
}
static generatePersistedFoldersFormValuesFromPersistedFolders(persistedFolders, persistentVolumeClaims) {
const finalRes = _.map(persistedFolders, (folder) => {
const pvc = _.find(persistentVolumeClaims, (item) => _.startsWith(item.Name, folder.PersistentVolumeClaimName));
const res = new KubernetesApplicationPersistedFolderFormValue(pvc.StorageClass);
res.PersistentVolumeClaimName = folder.PersistentVolumeClaimName;
res.Size = parseInt(pvc.Storage.slice(0, -2));
res.SizeUnit = pvc.Storage.slice(-2);
res.ContainerPath = folder.MountPath;
return res;
});
return finalRes;
}
static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts) {
const finalRes = _.map(publishedPorts, (port) => {
const res = new KubernetesApplicationPublishedPortFormValue();
res.Protocol = port.protocol;
res.ContainerPort = port.targetPort;
if (serviceType === KubernetesServiceTypes.LOAD_BALANCER) {
res.LoadBalancerPort = port.port;
res.LoadBalancerNodePort = port.nodePort;
} else if (serviceType === KubernetesServiceTypes.NODE_PORT) {
res.NodePort = port.nodePort;
}
return res;
});
return finalRes;
}
/**
* !APPLICATION TO FORMVALUES FUNCTIONS
*/
static isExternalApplication(application) {
return !application.ApplicationOwner;
}
}
export default KubernetesApplicationHelper;

View file

@ -0,0 +1,76 @@
import angular from 'angular';
import _ from 'lodash-es';
import PortainerError from 'Portainer/error';
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
import { KubernetesSystem_DefaultDeploymentUniqueLabelKey, KubernetesSystem_AnnotationsToSkip } from 'Kubernetes/models/history/models';
class KubernetesApplicationRollbackHelper {
static getPatchPayload(application, targetRevision) {
let result;
switch (application.ApplicationType) {
case KubernetesApplicationTypes.DEPLOYMENT:
result = KubernetesApplicationRollbackHelper._getDeploymentPayload(application, targetRevision);
break;
case KubernetesApplicationTypes.DAEMONSET:
result = KubernetesApplicationRollbackHelper._getDaemonSetPayload(application, targetRevision);
break;
case KubernetesApplicationTypes.STATEFULSET:
result = KubernetesApplicationRollbackHelper._getStatefulSetPayload(application, targetRevision);
break;
default:
throw new PortainerError('Unable to determine which association to use');
}
return result;
}
static _getDeploymentPayload(deploymentApp, targetRevision) {
const target = angular.copy(targetRevision);
const deployment = deploymentApp.Raw;
// remove hash label before patching back into the deployment
delete target.spec.template.metadata.labels[KubernetesSystem_DefaultDeploymentUniqueLabelKey];
// compute deployment annotations
const annotations = {};
_.forEach(KubernetesSystem_AnnotationsToSkip, (_, k) => {
const v = deployment.metadata.annotations[k];
if (v) {
annotations[k] = v;
}
});
_.forEach(target.metadata.annotations, (v, k) => {
if (!KubernetesSystem_AnnotationsToSkip[k]) {
annotations[k] = v;
}
});
// Create a patch of the Deployment that replaces spec.template
const patch = [
{
op: 'replace',
path: '/spec/template',
value: target.spec.template,
},
{
op: 'replace',
path: '/metadata/annotations',
value: annotations,
},
];
return patch;
}
static _getDaemonSetPayload(daemonSet, targetRevision) {
void daemonSet;
return targetRevision.data;
}
static _getStatefulSetPayload(statefulSet, targetRevision) {
void statefulSet;
return targetRevision.data;
}
}
export default KubernetesApplicationRollbackHelper;

View file

@ -0,0 +1,12 @@
import _ from 'lodash-es';
class KubernetesCommonHelper {
static assignOrDeleteIfEmpty(obj, path, value) {
if (!value || (value instanceof Array && !value.length)) {
_.unset(obj, path);
} else {
_.set(obj, path, value);
}
}
}
export default KubernetesCommonHelper;

View file

@ -0,0 +1,36 @@
import _ from 'lodash-es';
import { KubernetesPortainerConfigMapAccessKey } from 'Kubernetes/models/config-map/models';
import { UserAccessViewModel, TeamAccessViewModel } from 'Portainer/models/access';
class KubernetesConfigMapHelper {
static parseJSONData(configMap) {
_.forIn(configMap.Data, (value, key) => {
try {
configMap.Data[key] = JSON.parse(value);
} catch (err) {
configMap.Data[key] = value;
}
});
return configMap;
}
static modifiyNamespaceAccesses(configMap, namespace, accesses) {
configMap.Data[KubernetesPortainerConfigMapAccessKey][namespace] = {
UserAccessPolicies: {},
TeamAccessPolicies: {},
};
_.forEach(accesses, (item) => {
if (item instanceof UserAccessViewModel) {
configMap.Data[KubernetesPortainerConfigMapAccessKey][namespace].UserAccessPolicies[item.Id] = { RoleId: 0 };
} else if (item instanceof TeamAccessViewModel) {
configMap.Data[KubernetesPortainerConfigMapAccessKey][namespace].TeamAccessPolicies[item.Id] = { RoleId: 0 };
}
});
_.forIn(configMap.Data, (value, key) => {
configMap.Data[key] = JSON.stringify(value);
});
return configMap;
}
}
export default KubernetesConfigMapHelper;

View file

@ -0,0 +1,40 @@
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import _ from 'lodash-es';
class KubernetesConfigurationHelper {
static getUsingApplications(config, applications) {
return _.filter(applications, (app) => {
let envFind;
let volumeFind;
if (config.Type === KubernetesConfigurationTypes.CONFIGMAP) {
envFind = _.find(app.Env, { valueFrom: { configMapKeyRef: { name: config.Name } } });
volumeFind = _.find(app.Volumes, { configMap: { name: config.Name } });
} else {
envFind = _.find(app.Env, { valueFrom: { secretKeyRef: { name: config.Name } } });
volumeFind = _.find(app.Volumes, { secret: { secretName: config.Name } });
}
return envFind || volumeFind;
});
}
static isSystemToken(config) {
return _.startsWith(config.Name, 'default-token-');
}
static setConfigurationUsed(config) {
config.Used = config.Applications && config.Applications.length !== 0;
}
static setConfigurationsUsed(configurations, applications) {
_.forEach(configurations, (config) => {
config.Applications = KubernetesConfigurationHelper.getUsingApplications(config, applications);
KubernetesConfigurationHelper.setConfigurationUsed(config);
});
}
static isExternalConfiguration(configuration) {
return !configuration.ConfigurationOwner;
}
}
export default KubernetesConfigurationHelper;

View file

@ -0,0 +1,10 @@
import _ from 'lodash-es';
class KubernetesEventHelper {
static warningCount(events) {
const warnings = _.filter(events, (event) => event.Type === 'Warning');
return warnings.length;
}
}
export default KubernetesEventHelper;

View file

@ -0,0 +1,15 @@
import _ from 'lodash-es';
class KubernetesFormValidationHelper {
static getDuplicates(names) {
const groupped = _.groupBy(names);
const res = {};
_.forEach(names, (name, index) => {
if (groupped[name].length > 1 && name) {
res[index] = name;
}
});
return res;
}
}
export default KubernetesFormValidationHelper;

View file

@ -0,0 +1,27 @@
import _ from 'lodash-es';
class KubernetesDaemonSetHistoryHelper {
static _isControlledBy(daemonSet) {
return (item) => _.find(item.metadata.ownerReferences, { uid: daemonSet.metadata.uid }) !== undefined;
}
static filterOwnedRevisions(crList, daemonSet) {
// filter ControllerRevisions that has the same selector as the DaemonSet
// NOTE : this should be done in HTTP request based on daemonSet.spec.selector.matchLabels
// instead of getting all CR and filtering them here
const sameLabelsCR = _.filter(crList, ['metadata.labels', daemonSet.spec.selector.matchLabels]);
// Only include the RS whose ControllerRef matches the DaemonSet.
const controlledCR = _.filter(sameLabelsCR, KubernetesDaemonSetHistoryHelper._isControlledBy(daemonSet));
// sorts the list of ControllerRevisions by revision, using the creationTimestamp as a tie breaker (old to new)
const sortedList = _.sortBy(controlledCR, ['revision', 'metadata.creationTimestamp']);
return sortedList;
}
// getCurrentRS returns the newest CR the given daemonSet targets (latest version)
static getCurrentRevision(crList) {
const current = _.last(crList);
return current;
}
}
export default KubernetesDaemonSetHistoryHelper;

View file

@ -0,0 +1,56 @@
import _ from 'lodash-es';
import angular from 'angular';
import { KubernetesSystem_DefaultDeploymentUniqueLabelKey, KubernetesSystem_RevisionAnnotation } from 'Kubernetes/models/history/models';
class KubernetesDeploymentHistoryHelper {
static _isControlledBy(deployment) {
return (item) => _.find(item.metadata.ownerReferences, { uid: deployment.metadata.uid }) !== undefined;
}
static filterOwnedRevisions(rsList, deployment) {
// filter RS that has the same selector as the Deployment
// NOTE : this should be done in HTTP request based on deployment.spec.selector
// instead of getting all RS and filtering them here
const sameLabelsRS = _.filter(rsList, ['spec.selector', deployment.spec.selector]);
// Only include the RS whose ControllerRef matches the Deployment.
const controlledRS = _.filter(sameLabelsRS, KubernetesDeploymentHistoryHelper._isControlledBy(deployment));
// sorts the list of ReplicaSet by creation timestamp, using the names as a tie breaker (old to new)
const sortedList = _.sortBy(controlledRS, ['metadata.creationTimestamp', 'metadata.name']);
return sortedList;
}
// getCurrentRS returns the new RS the given deployment targets (the one with the same pod template).
static getCurrentRevision(rsListOriginal, deployment) {
const rsList = angular.copy(rsListOriginal);
// In rare cases, such as after cluster upgrades, Deployment may end up with
// having more than one new ReplicaSets that have the same template as its template,
// see https://github.com/kubernetes/kubernetes/issues/40415
// We deterministically choose the oldest new ReplicaSet (first match)
const current = _.find(rsList, (item) => {
// returns true if two given template.spec are equal, ignoring the diff in value of Labels[pod-template-hash]
// We ignore pod-template-hash because:
// 1. The hash result would be different upon podTemplateSpec API changes
// (e.g. the addition of a new field will cause the hash code to change)
// 2. The deployment template won't have hash labels
delete item.spec.template.metadata.labels[KubernetesSystem_DefaultDeploymentUniqueLabelKey];
return _.isEqual(deployment.spec.template, item.spec.template);
});
current.revision = current.metadata.annotations[KubernetesSystem_RevisionAnnotation];
return current;
}
// filters the RSList to drop all RS that have never been a version of the Deployment
// also add the revision as a field inside the RS
// Note: this should not impact rollback process as we only patch
// metadata.annotations and spec.template
static filterVersionedRevisions(rsList) {
const filteredRS = _.filter(rsList, (item) => item.metadata.annotations[KubernetesSystem_RevisionAnnotation] !== undefined);
return _.map(filteredRS, (item) => {
item.revision = item.metadata.annotations[KubernetesSystem_RevisionAnnotation];
return item;
});
}
}
export default KubernetesDeploymentHistoryHelper;

View file

@ -0,0 +1,50 @@
import _ from 'lodash-es';
import PortainerError from 'Portainer/error';
import KubernetesDeploymentHistoryHelper from 'Kubernetes/helpers/history/deployment';
import KubernetesDaemonSetHistoryHelper from 'Kubernetes/helpers/history/daemonset';
import KubernetesStatefulSetHistoryHelper from 'Kubernetes/helpers/history/statefulset';
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
class KubernetesHistoryHelper {
static getRevisions(rawRevisions, application) {
let currentRevision, revisionsList;
switch (application.ApplicationType) {
case KubernetesApplicationTypes.DEPLOYMENT:
[currentRevision, revisionsList] = KubernetesHistoryHelper._getDeploymentRevisions(rawRevisions, application.Raw);
break;
case KubernetesApplicationTypes.DAEMONSET:
[currentRevision, revisionsList] = KubernetesHistoryHelper._getDaemonSetRevisions(rawRevisions, application.Raw);
break;
case KubernetesApplicationTypes.STATEFULSET:
[currentRevision, revisionsList] = KubernetesHistoryHelper._getStatefulSetRevisions(rawRevisions, application.Raw);
break;
default:
throw new PortainerError('Unable to determine which association to use');
}
revisionsList = _.sortBy(revisionsList, 'revision');
return [currentRevision, revisionsList];
}
static _getDeploymentRevisions(rsList, deployment) {
const appRS = KubernetesDeploymentHistoryHelper.filterOwnedRevisions(rsList, deployment);
const currentRS = KubernetesDeploymentHistoryHelper.getCurrentRevision(appRS, deployment);
const versionedRS = KubernetesDeploymentHistoryHelper.filterVersionedRevisions(appRS);
return [currentRS, versionedRS];
}
static _getDaemonSetRevisions(crList, daemonSet) {
const appCR = KubernetesDaemonSetHistoryHelper.filterOwnedRevisions(crList, daemonSet);
const currentCR = KubernetesDaemonSetHistoryHelper.getCurrentRevision(appCR, daemonSet);
return [currentCR, appCR];
}
static _getStatefulSetRevisions(crList, statefulSet) {
const appCR = KubernetesStatefulSetHistoryHelper.filterOwnedRevisions(crList, statefulSet);
const currentCR = KubernetesStatefulSetHistoryHelper.getCurrentRevision(appCR, statefulSet);
return [currentCR, appCR];
}
}
export default KubernetesHistoryHelper;

View file

@ -0,0 +1,27 @@
import _ from 'lodash-es';
class KubernetesStatefulSetHistoryHelper {
static _isControlledBy(statefulSet) {
return (item) => _.find(item.metadata.ownerReferences, { uid: statefulSet.metadata.uid }) !== undefined;
}
static filterOwnedRevisions(crList, statefulSet) {
// filter ControllerRevisions that has the same selector as the StatefulSet
// NOTE : this should be done in HTTP request based on statefulSet.spec.selector.matchLabels
// instead of getting all CR and filtering them here
const sameLabelsCR = _.filter(crList, ['metadata.labels', statefulSet.spec.selector.matchLabels]);
// Only include the RS whose ControllerRef matches the StatefulSet.
const controlledCR = _.filter(sameLabelsCR, KubernetesStatefulSetHistoryHelper._isControlledBy(statefulSet));
// sorts the list of ControllerRevisions by revision, using the creationTimestamp as a tie breaker (old to new)
const sortedList = _.sortBy(controlledCR, ['revision', 'metadata.creationTimestamp']);
return sortedList;
}
// getCurrentRS returns the newest CR the given statefulSet targets (latest version)
static getCurrentRevision(crList) {
const current = _.last(crList);
return current;
}
}
export default KubernetesStatefulSetHistoryHelper;

View file

@ -0,0 +1,15 @@
import _ from 'lodash-es';
import angular from 'angular';
class KubernetesNamespaceHelper {
constructor(KUBERNETES_SYSTEM_NAMESPACES) {
this.KUBERNETES_SYSTEM_NAMESPACES = KUBERNETES_SYSTEM_NAMESPACES;
}
isSystemNamespace(namespace) {
return _.includes(this.KUBERNETES_SYSTEM_NAMESPACES, namespace);
}
}
export default KubernetesNamespaceHelper;
angular.module('portainer.app').service('KubernetesNamespaceHelper', KubernetesNamespaceHelper);

View file

@ -0,0 +1,9 @@
import { KubernetesPortainerResourceQuotaPrefix } from 'Kubernetes/models/resource-quota/models';
class KubernetesResourceQuotaHelper {
static generateResourceQuotaName(name) {
return KubernetesPortainerResourceQuotaPrefix + name;
}
}
export default KubernetesResourceQuotaHelper;

View file

@ -0,0 +1,44 @@
import _ from 'lodash-es';
import filesizeParser from 'filesize-parser';
import { KubernetesResourceReservation } from 'Kubernetes/models/resource-reservation/models';
class KubernetesResourceReservationHelper {
static computeResourceReservation(pods) {
const containers = _.reduce(pods, (acc, pod) => _.concat(acc, pod.Containers), []);
return _.reduce(
containers,
(acc, container) => {
if (container.resources && container.resources.requests) {
if (container.resources.requests.memory) {
acc.Memory += filesizeParser(container.resources.requests.memory, { base: 10 });
}
if (container.resources.requests.cpu) {
acc.CPU += KubernetesResourceReservationHelper.parseCPU(container.resources.requests.cpu);
}
}
return acc;
},
new KubernetesResourceReservation()
);
}
static parseCPU(cpu) {
let res = parseInt(cpu);
if (_.endsWith(cpu, 'm')) {
res /= 1000;
}
return res;
}
static megaBytesValue(value) {
return Math.floor(filesizeParser(value) / 1000 / 1000);
}
static bytesValue(mem) {
return filesizeParser(mem) * 1000 * 1000;
}
}
export default KubernetesResourceReservationHelper;

View file

@ -0,0 +1,13 @@
import _ from 'lodash-es';
import { KubernetesServiceHeadlessPrefix } from 'Kubernetes/models/service/models';
class KubernetesServiceHelper {
static generateHeadlessServiceName(name) {
return KubernetesServiceHeadlessPrefix + name;
}
static findApplicationBoundService(services, rawApp) {
return _.find(services, (item) => _.isMatch(rawApp.spec.template.metadata.labels, item.spec.selector));
}
}
export default KubernetesServiceHelper;

View file

@ -0,0 +1,26 @@
import _ from 'lodash-es';
import { KubernetesStack } from 'Kubernetes/models/stack/models';
class KubernetesStackHelper {
static stacksFromApplications(applications) {
const res = _.reduce(
applications,
(acc, app) => {
if (app.StackName !== '-') {
let stack = _.find(acc, { Name: app.StackName, ResourcePool: app.ResourcePool });
if (!stack) {
stack = new KubernetesStack();
stack.Name = app.StackName;
stack.ResourcePool = app.ResourcePool;
acc.push(stack);
}
stack.Applications.push(app);
}
return acc;
},
[]
);
return res;
}
}
export default KubernetesStackHelper;

View file

@ -0,0 +1,37 @@
import _ from 'lodash-es';
import uuidv4 from 'uuid/v4';
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
class KubernetesVolumeHelper {
// TODO: review
// the following condition
// && (app.ApplicationType === KubernetesApplicationTypes.STATEFULSET ? _.includes(volume.PersistentVolumeClaim.Name, app.Name) : true);
// is made to enforce finding the good SFS when multiple SFS in the same namespace
// are referencing an internal PVC using the same internal name
// (PVC are not exposed to other apps so they can have the same name in differents SFS)
static getUsingApplications(volume, applications) {
return _.filter(applications, (app) => {
const names = _.without(_.map(app.Volumes, 'persistentVolumeClaim.claimName'), undefined);
const matchingNames = _.filter(names, (name) => _.startsWith(volume.PersistentVolumeClaim.Name, name));
return (
volume.ResourcePool.Namespace.Name === app.ResourcePool &&
matchingNames.length &&
(app.ApplicationType === KubernetesApplicationTypes.STATEFULSET ? _.includes(volume.PersistentVolumeClaim.Name, app.Name) : true)
);
});
}
static isUsed(item) {
return item.Applications.length !== 0;
}
static generatedApplicationConfigVolumeName(name) {
return 'config-' + name + '-' + uuidv4();
}
static isExternalVolume(volume) {
return !volume.PersistentVolumeClaim.ApplicationOwner;
}
}
export default KubernetesVolumeHelper;

View file

@ -0,0 +1,23 @@
import { KubernetesHorizontalPodAutoScaler } from './models';
export class KubernetesHorizontalPodAutoScalerConverter {
/**
* Convert API data to KubernetesHorizontalPodAutoScaler model
*/
static apiToModel(data, yaml) {
const res = new KubernetesHorizontalPodAutoScaler();
res.Id = data.metadata.uid;
res.Namespace = data.metadata.namespace;
res.Name = data.metadata.name;
res.MinReplicas = data.spec.minReplicas;
res.MaxReplicas = data.spec.maxReplicas;
res.TargetCPUUtilizationPercentage = data.spec.targetCPUUtilizationPercentage;
if (data.spec.scaleTargetRef) {
res.TargetEntity.ApiVersion = data.spec.scaleTargetRef.apiVersion;
res.TargetEntity.Kind = data.spec.scaleTargetRef.kind;
res.TargetEntity.Name = data.spec.scaleTargetRef.name;
}
res.Yaml = yaml ? yaml.data : '';
return res;
}
}

View file

@ -0,0 +1,26 @@
import _ from 'lodash-es';
import PortainerError from 'Portainer/error';
import { KubernetesApplication, KubernetesApplicationTypes, KubernetesApplicationTypeStrings } from 'Kubernetes/models/application/models';
import { KubernetesDeployment } from 'Kubernetes/models/deployment/models';
import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models';
import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models';
function _getApplicationTypeString(app) {
if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT) || app instanceof KubernetesDeployment) {
return KubernetesApplicationTypeStrings.DEPLOYMENT;
} else if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DAEMONSET) || app instanceof KubernetesDaemonSet) {
return KubernetesApplicationTypeStrings.DAEMONSET;
} else if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.STATEFULSET) || app instanceof KubernetesStatefulSet) {
return KubernetesApplicationTypeStrings.STATEFULSET;
// } else if () { ---> TODO: refactor - handle bare pod type !
} else {
throw new PortainerError('Unable to determine application type');
}
}
export class KubernetesHorizontalPodAutoScalerHelper {
static findApplicationBoundScaler(sList, app) {
const kind = _getApplicationTypeString(app);
return _.find(sList, (item) => item.TargetEntity.Kind === kind && item.TargetEntity.Name === app.Name);
}
}

View file

@ -0,0 +1,23 @@
/**
* KubernetesHorizontalPodAutoScaler Model
*/
const _KubernetesHorizontalPodAutoScaler = Object.freeze({
Id: '',
Namespace: '',
Name: '',
MinReplicas: 1,
MaxReplicas: 1,
TargetCPUUtilizationPercentage: undefined,
TargetEntity: {
ApiVersion: '',
Kind: '',
Name: '',
},
Yaml: '',
});
export class KubernetesHorizontalPodAutoScaler {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesHorizontalPodAutoScaler)));
}
}

View file

@ -0,0 +1,50 @@
import { rawResponse } from 'Kubernetes/rest/response/transform';
angular.module('portainer.kubernetes').factory('KubernetesHorizontalPodAutoScalers', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function KubernetesHorizontalPodAutoScalersFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return function (namespace) {
const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/autoscaling/v1' + (namespace ? '/namespaces/:namespace' : '') + '/horizontalpodautoscalers/:id/:action';
return $resource(
url,
{
endpointId: EndpointProvider.endpointID,
namespace: namespace,
},
{
get: {
method: 'GET',
timeout: 15000,
ignoreLoadingBar: true,
},
getYaml: {
method: 'GET',
headers: {
Accept: 'application/yaml',
},
transformResponse: rawResponse,
ignoreLoadingBar: true,
},
create: { method: 'POST' },
update: { method: 'PUT' },
patch: {
method: 'PATCH',
headers: {
'Content-Type': 'application/json-patch+json',
},
},
rollback: {
method: 'PATCH',
headers: {
'Content-Type': 'application/json-patch+json',
},
},
delete: { method: 'DELETE' },
}
);
};
},
]);

View file

@ -0,0 +1,135 @@
import angular from 'angular';
import _ from 'lodash-es';
import PortainerError from 'Portainer/error';
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
import { KubernetesHorizontalPodAutoScalerConverter } from './converter';
class KubernetesHorizontalPodAutoScalerService {
/* @ngInject */
constructor($async, KubernetesHorizontalPodAutoScalers) {
this.$async = $async;
this.KubernetesHorizontalPodAutoScalers = KubernetesHorizontalPodAutoScalers;
this.getAsync = this.getAsync.bind(this);
this.getAllAsync = this.getAllAsync.bind(this);
// this.createAsync = this.createAsync.bind(this);
// this.patchAsync = this.patchAsync.bind(this);
// this.rollbackAsync = this.rollbackAsync.bind(this);
// this.deleteAsync = this.deleteAsync.bind(this);
}
/**
* GET
*/
async getAsync(namespace, name) {
try {
const params = new KubernetesCommonParams();
params.id = name;
const [raw, yaml] = await Promise.all([
this.KubernetesHorizontalPodAutoScalers(namespace).get(params).$promise,
this.KubernetesHorizontalPodAutoScalers(namespace).getYaml(params).$promise,
]);
const res = KubernetesHorizontalPodAutoScalerConverter.apiToModel(raw, yaml);
return res;
} catch (err) {
throw new PortainerError('Unable to retrieve HorizontalPodAutoScaler', err);
}
}
async getAllAsync(namespace) {
try {
const data = await this.KubernetesHorizontalPodAutoScalers(namespace).get().$promise;
const res = _.map(data.items, (item) => KubernetesHorizontalPodAutoScalerConverter.apiToModel(item));
return res;
} catch (err) {
throw new PortainerError('Unable to retrieve HorizontalPodAutoScalers', err);
}
}
get(namespace, name) {
if (name) {
return this.$async(this.getAsync, namespace, name);
}
return this.$async(this.getAllAsync, namespace);
}
// /**
// * CREATE
// */
// async createAsync(horizontalPodAutoScaler) {
// try {
// const params = {};
// const payload = KubernetesHorizontalPodAutoScalerConverter.createPayload(horizontalPodAutoScaler);
// const namespace = payload.metadata.namespace;
// const data = await this.KubernetesHorizontalPodAutoScalers(namespace).create(params, payload).$promise;
// return data;
// } catch (err) {
// throw new PortainerError('Unable to create horizontalPodAutoScaler', err);
// }
// }
// create(horizontalPodAutoScaler) {
// return this.$async(this.createAsync, horizontalPodAutoScaler);
// }
// /**
// * PATCH
// */
// async patchAsync(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler) {
// try {
// const params = new KubernetesCommonParams();
// params.id = newHorizontalPodAutoScaler.Name;
// const namespace = newHorizontalPodAutoScaler.Namespace;
// const payload = KubernetesHorizontalPodAutoScalerConverter.patchPayload(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler);
// if (!payload.length) {
// return;
// }
// const data = await this.KubernetesHorizontalPodAutoScalers(namespace).patch(params, payload).$promise;
// return data;
// } catch (err) {
// throw new PortainerError('Unable to patch horizontalPodAutoScaler', err);
// }
// }
// patch(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler) {
// return this.$async(this.patchAsync, oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler);
// }
// /**
// * DELETE
// */
// async deleteAsync(horizontalPodAutoScaler) {
// try {
// const params = new KubernetesCommonParams();
// params.id = horizontalPodAutoScaler.Name;
// const namespace = horizontalPodAutoScaler.Namespace;
// await this.KubernetesHorizontalPodAutoScalers(namespace).delete(params).$promise;
// } catch (err) {
// throw new PortainerError('Unable to remove horizontalPodAutoScaler', err);
// }
// }
// delete(horizontalPodAutoScaler) {
// return this.$async(this.deleteAsync, horizontalPodAutoScaler);
// }
// /**
// * ROLLBACK
// */
// async rollbackAsync(namespace, name, payload) {
// try {
// const params = new KubernetesCommonParams();
// params.id = name;
// await this.KubernetesHorizontalPodAutoScalers(namespace).rollback(params, payload).$promise;
// } catch (err) {
// throw new PortainerError('Unable to rollback horizontalPodAutoScaler', err);
// }
// }
// rollback(namespace, name, payload) {
// return this.$async(this.rollbackAsync, namespace, name, payload);
// }
}
export default KubernetesHorizontalPodAutoScalerService;
angular.module('portainer.kubernetes').service('KubernetesHorizontalPodAutoScalerService', KubernetesHorizontalPodAutoScalerService);

View file

@ -0,0 +1,118 @@
import { KubernetesApplicationDeploymentTypes, KubernetesApplicationPublishingTypes, KubernetesApplicationDataAccessPolicies } from './models';
/**
* KubernetesApplicationFormValues Model
*/
const _KubernetesApplicationFormValues = Object.freeze({
ApplicationType: undefined, // will only exist for formValues generated from Application (app edit situation)
ResourcePool: {},
Name: '',
StackName: '',
ApplicationOwner: '',
Image: '',
ReplicaCount: 1,
Note: '',
EnvironmentVariables: [], // KubernetesApplicationEnvironmentVariableFormValue list
PersistedFolders: [], // KubernetesApplicationPersistedFolderFormValue list
PublishedPorts: [], // KubernetesApplicationPublishedPortFormValue list
MemoryLimit: 0,
CpuLimit: 0,
DeploymentType: KubernetesApplicationDeploymentTypes.REPLICATED,
PublishingType: KubernetesApplicationPublishingTypes.INTERNAL,
DataAccessPolicy: KubernetesApplicationDataAccessPolicies.SHARED,
Configurations: [], // KubernetesApplicationConfigurationFormValue list
});
export class KubernetesApplicationFormValues {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationFormValues)));
}
}
export const KubernetesApplicationConfigurationFormValueOverridenKeyTypes = Object.freeze({
ENVIRONMENT: 1,
FILESYSTEM: 2,
});
/**
* KubernetesApplicationConfigurationFormValueOverridenKey Model
*/
const _KubernetesApplicationConfigurationFormValueOverridenKey = Object.freeze({
Key: '',
Path: '',
Type: KubernetesApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT,
});
export class KubernetesApplicationConfigurationFormValueOverridenKey {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationConfigurationFormValueOverridenKey)));
}
}
/**
* KubernetesApplicationConfigurationFormValue Model
*/
const _KubernetesApplicationConfigurationFormValue = Object.freeze({
SelectedConfiguration: undefined,
Overriden: false,
OverridenKeys: [], // KubernetesApplicationConfigurationFormValueOverridenKey list
});
export class KubernetesApplicationConfigurationFormValue {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationConfigurationFormValue)));
}
}
/**
* KubernetesApplicationEnvironmentVariableFormValue Model
*/
const _KubernetesApplicationEnvironmentVariableFormValue = Object.freeze({
Name: '',
Value: '',
IsSecret: false,
NeedsDeletion: false,
IsNew: true,
});
export class KubernetesApplicationEnvironmentVariableFormValue {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationEnvironmentVariableFormValue)));
}
}
/**
* KubernetesApplicationPersistedFolderFormValue Model
*/
const _KubernetesApplicationPersistedFolderFormValue = Object.freeze({
PersistentVolumeClaimName: '', // will be empty for new volumes (create/edit app) and filled for existing ones (edit)
NeedsDeletion: false,
ContainerPath: '',
Size: '',
SizeUnit: 'GB',
StorageClass: {},
});
export class KubernetesApplicationPersistedFolderFormValue {
constructor(storageClass) {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPersistedFolderFormValue)));
this.StorageClass = storageClass;
}
}
/**
* KubernetesApplicationPublishedPortFormValue Model
*/
const _KubernetesApplicationPublishedPortFormValue = Object.freeze({
ContainerPort: '',
NodePort: '',
LoadBalancerPort: '',
LoadBalancerNodePort: undefined, // only filled to save existing loadbalancer nodePort and drop it when moving app exposure from LB to Internal/NodePort
Protocol: 'TCP',
});
export class KubernetesApplicationPublishedPortFormValue {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPublishedPortFormValue)));
}
}

View file

@ -0,0 +1,114 @@
export const KubernetesApplicationDeploymentTypes = Object.freeze({
REPLICATED: 1,
GLOBAL: 2,
});
export const KubernetesApplicationDataAccessPolicies = Object.freeze({
SHARED: 1,
ISOLATED: 2,
});
export const KubernetesApplicationTypes = Object.freeze({
DEPLOYMENT: 1,
DAEMONSET: 2,
STATEFULSET: 3,
});
export const KubernetesApplicationTypeStrings = Object.freeze({
DEPLOYMENT: 'Deployment',
DAEMONSET: 'DaemonSet',
STATEFULSET: 'StatefulSet',
});
export const KubernetesApplicationPublishingTypes = Object.freeze({
INTERNAL: 1,
CLUSTER: 2,
LOAD_BALANCER: 3,
});
export const KubernetesApplicationQuotaDefaults = {
CpuLimit: 0.1,
MemoryLimit: 64, // MB
};
export const KubernetesPortainerApplicationStackNameLabel = 'io.portainer.kubernetes.application.stack';
export const KubernetesPortainerApplicationNameLabel = 'io.portainer.kubernetes.application.name';
export const KubernetesPortainerApplicationOwnerLabel = 'io.portainer.kubernetes.application.owner';
export const KubernetesPortainerApplicationNote = 'io.portainer.kubernetes.application.note';
/**
* KubernetesApplication Model (Composite)
*/
const _KubernetesApplication = Object.freeze({
Id: '',
Name: '',
StackName: '',
ApplicationOwner: '',
ApplicationName: '',
ResourcePool: '',
Image: '',
CreationDate: 0,
Pods: [],
Limits: {},
ServiceType: '',
ServiceId: '',
ServiceName: '',
HeadlessServiceName: undefined, // only used for StatefulSet
LoadBalancerIPAddress: undefined, // only filled when bound service is LoadBalancer and state is available
PublishedPorts: [],
Volumes: [],
Env: [],
PersistedFolders: [], // KubernetesApplicationPersistedFolder list
ConfigurationVolumes: [], // KubernetesApplicationConfigurationVolume list
DeploymentType: 'Unknown',
DataAccessPolicy: 'Unknown',
ApplicationType: 'Unknown',
RunningPodsCount: 0,
TotalPodsCount: 0,
Yaml: '',
Note: '',
Revisions: undefined,
CurrentRevision: undefined,
Raw: undefined, // only filled when inspecting app details / create / edit view (never filled in multiple-apps views)
AutoScaler: undefined, // only filled if the application has an HorizontalPodAutoScaler bound to it
});
export class KubernetesApplication {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplication)));
}
}
/**
* KubernetesApplicationPersistedFolder Model
*/
const _KubernetesApplicationPersistedFolder = Object.freeze({
MountPath: '',
PersistentVolumeClaimName: '',
HostPath: '',
});
export class KubernetesApplicationPersistedFolder {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPersistedFolder)));
}
}
/**
* KubernetesApplicationConfigurationVolume Model
*/
const _KubernetesApplicationConfigurationVolume = Object.freeze({
fileMountPath: '',
rootMountPath: '',
configurationKey: '',
configurationName: '',
});
export class KubernetesApplicationConfigurationVolume {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationConfigurationVolume)));
}
}

View file

@ -0,0 +1,142 @@
/////////////////////////// VOLUME MOUNT ///////////////////////////////
/**
* KubernetesApplicationVolumeMount Model
*/
const _KubernetesApplicationVolumeMount = Object.freeze({
name: '',
mountPath: '',
readOnly: false,
});
export class KubernetesApplicationVolumeMountPayload {
constructor(readOnly) {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationVolumeMount)));
if (readOnly) {
this.readOnly = true;
} else {
delete this.readOnly;
}
}
}
///////////////////////////////// PVC /////////////////////////////////
/**
* KubernetesApplicationVolumePersistentPayload Model
*/
const _KubernetesApplicationVolumePersistentPayload = Object.freeze({
name: '',
persistentVolumeClaim: {
claimName: '',
},
});
export class KubernetesApplicationVolumePersistentPayload {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationVolumePersistentPayload)));
}
}
/////////////////////////////// CONFIG AS VOLUME ////////////////////////
/**
* KubernetesApplicationVolumeConfigMapPayload Model
*/
const _KubernetesApplicationVolumeConfigMapPayload = Object.freeze({
name: '',
configMap: {
name: '',
items: [], // KubernetesApplicationVolumeEntryPayload
},
});
export class KubernetesApplicationVolumeConfigMapPayload {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationVolumeConfigMapPayload)));
}
}
//////////////////// SECRET AS VOLUME /////////////////////////////////////
/**
* KubernetesApplicationVolumeSecretPayload Model
*/
const _KubernetesApplicationVolumeSecretPayload = Object.freeze({
name: '',
secret: {
secretName: '',
items: [], // KubernetesApplicationVolumeEntryPayload
},
});
export class KubernetesApplicationVolumeSecretPayload {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationVolumeSecretPayload)));
}
}
/**
* KubernetesApplicationVolumeEntryPayload Model
*/
const _KubernetesApplicationVolumeEntryPayload = Object.freeze({
key: '',
path: '',
});
export class KubernetesApplicationVolumeEntryPayload {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationVolumeEntryPayload)));
}
}
//////////////////////////// ENV ENTRY //////////////////////////////
/**
* KubernetesApplicationEnvPayload Model
*/
const _KubernetesApplicationEnvPayload = Object.freeze({
name: '',
value: '',
});
export class KubernetesApplicationEnvPayload {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationEnvPayload)));
}
}
///////////////////////// CONFIG AS ENV ////////////////////////////////
/**
* KubernetesApplicationEnvConfigMapPayload Model
*/
const _KubernetesApplicationEnvConfigMapPayload = Object.freeze({
name: '',
valueFrom: {
configMapKeyRef: {
name: '',
key: '',
},
},
});
export class KubernetesApplicationEnvConfigMapPayload {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationEnvConfigMapPayload)));
}
}
//////////////////////// SECRET AS ENV //////////////////////////////////
/**
* KubernetesApplicationEnvSecretPayload Model
*/
const _KubernetesApplicationEnvSecretPayload = Object.freeze({
name: '',
valueFrom: {
secretKeyRef: {
name: '',
key: '',
},
},
});
export class KubernetesApplicationEnvSecretPayload {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationEnvSecretPayload)));
}
}

View file

@ -0,0 +1,11 @@
/**
* Generic params
*/
const _KubernetesCommonParams = Object.freeze({
id: '',
});
export class KubernetesCommonParams {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesCommonParams)));
}
}

View file

@ -0,0 +1,15 @@
/**
* Generic metadata payload
*/
const _KubernetesCommonMetadataPayload = Object.freeze({
uid: '',
name: '',
namespace: '',
labels: {},
annotations: {},
});
export class KubernetesCommonMetadataPayload {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesCommonMetadataPayload)));
}
}

Some files were not shown because too many files have changed in this diff Show more