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:
parent
24528ecea8
commit
af6bea5acc
361 changed files with 20794 additions and 2349 deletions
261
app/kubernetes/__module.js
Normal file
261
app/kubernetes/__module.js
Normal 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);
|
||||
},
|
||||
]);
|
|
@ -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>
|
|
@ -0,0 +1,12 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesPodsDatatable', {
|
||||
templateUrl: './podsDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
refreshCallback: '<',
|
||||
},
|
||||
});
|
|
@ -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>
|
|
@ -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: '<',
|
||||
},
|
||||
});
|
|
@ -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();
|
||||
};
|
||||
},
|
||||
]);
|
|
@ -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>
|
|
@ -0,0 +1,13 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesApplicationsPortsDatatable', {
|
||||
templateUrl: './applicationsPortsDatatable.html',
|
||||
controller: 'KubernetesApplicationsPortsDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
refreshCallback: '<',
|
||||
},
|
||||
});
|
|
@ -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();
|
||||
};
|
||||
},
|
||||
]);
|
|
@ -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>
|
|
@ -0,0 +1,14 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesApplicationsStacksDatatable', {
|
||||
templateUrl: './applicationsStacksDatatable.html',
|
||||
controller: 'KubernetesApplicationsStacksDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
refreshCallback: '<',
|
||||
removeAction: '<',
|
||||
},
|
||||
});
|
|
@ -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();
|
||||
};
|
||||
},
|
||||
]);
|
|
@ -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>
|
|
@ -0,0 +1,13 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesConfigurationsDatatable', {
|
||||
templateUrl: './configurationsDatatable.html',
|
||||
controller: 'KubernetesConfigurationsDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
refreshCallback: '<',
|
||||
removeAction: '<',
|
||||
},
|
||||
});
|
|
@ -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();
|
||||
};
|
||||
},
|
||||
]);
|
|
@ -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>
|
|
@ -0,0 +1,14 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesEventsDatatable', {
|
||||
templateUrl: './eventsDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
loading: '<',
|
||||
refreshCallback: '<',
|
||||
},
|
||||
});
|
|
@ -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>
|
|
@ -0,0 +1,13 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesIntegratedApplicationsDatatable', {
|
||||
templateUrl: './integratedApplicationsDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
refreshCallback: '<',
|
||||
},
|
||||
});
|
|
@ -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>
|
|
@ -0,0 +1,13 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesNodeApplicationsDatatable', {
|
||||
templateUrl: './nodeApplicationsDatatable.html',
|
||||
controller: 'KubernetesNodeApplicationsDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
refreshCallback: '<',
|
||||
},
|
||||
});
|
|
@ -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();
|
||||
};
|
||||
},
|
||||
]);
|
|
@ -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>
|
|
@ -0,0 +1,13 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesNodesDatatable', {
|
||||
templateUrl: './nodesDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
refreshCallback: '<',
|
||||
isAdmin: '<',
|
||||
},
|
||||
});
|
|
@ -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>
|
|
@ -0,0 +1,13 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesResourcePoolApplicationsDatatable', {
|
||||
templateUrl: './resourcePoolApplicationsDatatable.html',
|
||||
controller: 'KubernetesResourcePoolApplicationsDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
refreshCallback: '<',
|
||||
},
|
||||
});
|
|
@ -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();
|
||||
};
|
||||
},
|
||||
]);
|
|
@ -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>
|
|
@ -0,0 +1,14 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesResourcePoolsDatatable', {
|
||||
templateUrl: './resourcePoolsDatatable.html',
|
||||
controller: 'KubernetesResourcePoolsDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
removeAction: '<',
|
||||
refreshCallback: '<',
|
||||
},
|
||||
});
|
|
@ -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();
|
||||
};
|
||||
},
|
||||
]);
|
|
@ -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>
|
|
@ -0,0 +1,14 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesVolumesDatatable', {
|
||||
templateUrl: './volumesDatatable.html',
|
||||
controller: 'KubernetesVolumesDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
removeAction: '<',
|
||||
refreshCallback: '<',
|
||||
},
|
||||
});
|
|
@ -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);
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesFeedbackPanel', {
|
||||
templateUrl: './feedbackPanel.html',
|
||||
});
|
|
@ -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>
|
|
@ -0,0 +1,8 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesConfigurationData', {
|
||||
templateUrl: './kubernetesConfigurationData.html',
|
||||
controller: 'KubernetesConfigurationDataController',
|
||||
bindings: {
|
||||
formValues: '=',
|
||||
isValid: '=',
|
||||
},
|
||||
});
|
|
@ -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);
|
|
@ -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>
|
|
@ -0,0 +1,6 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesSidebarContent', {
|
||||
templateUrl: './kubernetesSidebarContent.html',
|
||||
bindings: {
|
||||
adminAccess: '<',
|
||||
},
|
||||
});
|
|
@ -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>
|
|
@ -0,0 +1,11 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesResourceReservation', {
|
||||
templateUrl: './resourceReservation.html',
|
||||
controller: 'KubernetesResourceReservationController',
|
||||
bindings: {
|
||||
description: '@',
|
||||
cpu: '<',
|
||||
cpuLimit: '<',
|
||||
memory: '<',
|
||||
memoryLimit: '<',
|
||||
},
|
||||
});
|
|
@ -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);
|
10
app/kubernetes/components/view-header/viewHeader.html
Normal file
10
app/kubernetes/components/view-header/viewHeader.html
Normal 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>
|
9
app/kubernetes/components/view-header/viewHeader.js
Normal file
9
app/kubernetes/components/view-header/viewHeader.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesViewHeader', {
|
||||
templateUrl: './viewHeader.html',
|
||||
transclude: true,
|
||||
bindings: {
|
||||
viewReady: '<',
|
||||
title: '@',
|
||||
state: '@',
|
||||
},
|
||||
});
|
8
app/kubernetes/components/view-loading/viewLoading.html
Normal file
8
app/kubernetes/components/view-loading/viewLoading.html
Normal 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>
|
6
app/kubernetes/components/view-loading/viewLoading.js
Normal file
6
app/kubernetes/components/view-loading/viewLoading.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesViewLoading', {
|
||||
templateUrl: './viewLoading.html',
|
||||
bindings: {
|
||||
viewReady: '<',
|
||||
},
|
||||
});
|
|
@ -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>
|
|
@ -0,0 +1,8 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesYamlInspector', {
|
||||
templateUrl: './yamlInspector.html',
|
||||
controller: 'KubernetesYamlInspectorController',
|
||||
bindings: {
|
||||
key: '@',
|
||||
data: '<',
|
||||
},
|
||||
});
|
|
@ -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);
|
302
app/kubernetes/converters/application.js
Normal file
302
app/kubernetes/converters/application.js
Normal 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;
|
81
app/kubernetes/converters/configMap.js
Normal file
81
app/kubernetes/converters/configMap.js
Normal 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;
|
31
app/kubernetes/converters/configuration.js
Normal file
31
app/kubernetes/converters/configuration.js
Normal 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;
|
79
app/kubernetes/converters/daemonSet.js
Normal file
79
app/kubernetes/converters/daemonSet.js
Normal 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;
|
80
app/kubernetes/converters/deployment.js
Normal file
80
app/kubernetes/converters/deployment.js
Normal 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;
|
15
app/kubernetes/converters/event.js
Normal file
15
app/kubernetes/converters/event.js
Normal 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;
|
29
app/kubernetes/converters/namespace.js
Normal file
29
app/kubernetes/converters/namespace.js
Normal 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;
|
65
app/kubernetes/converters/node.js
Normal file
65
app/kubernetes/converters/node.js
Normal 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;
|
68
app/kubernetes/converters/persistentVolumeClaim.js
Normal file
68
app/kubernetes/converters/persistentVolumeClaim.js
Normal 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;
|
32
app/kubernetes/converters/pod.js
Normal file
32
app/kubernetes/converters/pod.js
Normal 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;
|
12
app/kubernetes/converters/resourcePool.js
Normal file
12
app/kubernetes/converters/resourcePool.js
Normal 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;
|
87
app/kubernetes/converters/resourceQuota.js
Normal file
87
app/kubernetes/converters/resourceQuota.js
Normal 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;
|
58
app/kubernetes/converters/secret.js
Normal file
58
app/kubernetes/converters/secret.js
Normal 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;
|
88
app/kubernetes/converters/service.js
Normal file
88
app/kubernetes/converters/service.js
Normal 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;
|
84
app/kubernetes/converters/statefulSet.js
Normal file
84
app/kubernetes/converters/statefulSet.js
Normal 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;
|
14
app/kubernetes/converters/storageClass.js
Normal file
14
app/kubernetes/converters/storageClass.js
Normal 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;
|
12
app/kubernetes/converters/volume.js
Normal file
12
app/kubernetes/converters/volume.js
Normal 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;
|
72
app/kubernetes/filters/applicationFilters.js
Normal file
72
app/kubernetes/filters/applicationFilters.js
Normal 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.';
|
||||
}
|
||||
};
|
||||
});
|
13
app/kubernetes/filters/configurationFilters.js
Normal file
13
app/kubernetes/filters/configurationFilters.js
Normal 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';
|
||||
}
|
||||
};
|
||||
});
|
16
app/kubernetes/filters/eventFilters.js
Normal file
16
app/kubernetes/filters/eventFilters.js
Normal 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';
|
||||
}
|
||||
};
|
||||
});
|
11
app/kubernetes/filters/filters.js
Normal file
11
app/kubernetes/filters/filters.js
Normal 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';
|
||||
}
|
||||
};
|
||||
});
|
35
app/kubernetes/filters/nodeFilters.js
Normal file
35
app/kubernetes/filters/nodeFilters.js
Normal 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';
|
||||
}
|
||||
};
|
||||
});
|
84
app/kubernetes/filters/podFilters.js
Normal file
84
app/kubernetes/filters/podFilters.js
Normal 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';
|
||||
}
|
||||
};
|
||||
});
|
273
app/kubernetes/helpers/application/index.js
Normal file
273
app/kubernetes/helpers/application/index.js
Normal 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;
|
76
app/kubernetes/helpers/application/rollback.js
Normal file
76
app/kubernetes/helpers/application/rollback.js
Normal 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;
|
12
app/kubernetes/helpers/commonHelper.js
Normal file
12
app/kubernetes/helpers/commonHelper.js
Normal 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;
|
36
app/kubernetes/helpers/configMapHelper.js
Normal file
36
app/kubernetes/helpers/configMapHelper.js
Normal 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;
|
40
app/kubernetes/helpers/configurationHelper.js
Normal file
40
app/kubernetes/helpers/configurationHelper.js
Normal 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;
|
10
app/kubernetes/helpers/eventHelper.js
Normal file
10
app/kubernetes/helpers/eventHelper.js
Normal 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;
|
15
app/kubernetes/helpers/formValidationHelper.js
Normal file
15
app/kubernetes/helpers/formValidationHelper.js
Normal 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;
|
27
app/kubernetes/helpers/history/daemonset.js
Normal file
27
app/kubernetes/helpers/history/daemonset.js
Normal 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;
|
56
app/kubernetes/helpers/history/deployment.js
Normal file
56
app/kubernetes/helpers/history/deployment.js
Normal 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;
|
50
app/kubernetes/helpers/history/index.js
Normal file
50
app/kubernetes/helpers/history/index.js
Normal 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;
|
27
app/kubernetes/helpers/history/statefulset.js
Normal file
27
app/kubernetes/helpers/history/statefulset.js
Normal 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;
|
15
app/kubernetes/helpers/namespaceHelper.js
Normal file
15
app/kubernetes/helpers/namespaceHelper.js
Normal 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);
|
9
app/kubernetes/helpers/resourceQuotaHelper.js
Normal file
9
app/kubernetes/helpers/resourceQuotaHelper.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { KubernetesPortainerResourceQuotaPrefix } from 'Kubernetes/models/resource-quota/models';
|
||||
|
||||
class KubernetesResourceQuotaHelper {
|
||||
static generateResourceQuotaName(name) {
|
||||
return KubernetesPortainerResourceQuotaPrefix + name;
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesResourceQuotaHelper;
|
44
app/kubernetes/helpers/resourceReservationHelper.js
Normal file
44
app/kubernetes/helpers/resourceReservationHelper.js
Normal 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;
|
13
app/kubernetes/helpers/serviceHelper.js
Normal file
13
app/kubernetes/helpers/serviceHelper.js
Normal 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;
|
26
app/kubernetes/helpers/stackHelper.js
Normal file
26
app/kubernetes/helpers/stackHelper.js
Normal 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;
|
37
app/kubernetes/helpers/volumeHelper.js
Normal file
37
app/kubernetes/helpers/volumeHelper.js
Normal 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;
|
23
app/kubernetes/horizontal-pod-auto-scaler/converter.js
Normal file
23
app/kubernetes/horizontal-pod-auto-scaler/converter.js
Normal 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;
|
||||
}
|
||||
}
|
26
app/kubernetes/horizontal-pod-auto-scaler/helper.js
Normal file
26
app/kubernetes/horizontal-pod-auto-scaler/helper.js
Normal 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);
|
||||
}
|
||||
}
|
23
app/kubernetes/horizontal-pod-auto-scaler/models.js
Normal file
23
app/kubernetes/horizontal-pod-auto-scaler/models.js
Normal 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)));
|
||||
}
|
||||
}
|
50
app/kubernetes/horizontal-pod-auto-scaler/rest.js
Normal file
50
app/kubernetes/horizontal-pod-auto-scaler/rest.js
Normal 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' },
|
||||
}
|
||||
);
|
||||
};
|
||||
},
|
||||
]);
|
135
app/kubernetes/horizontal-pod-auto-scaler/service.js
Normal file
135
app/kubernetes/horizontal-pod-auto-scaler/service.js
Normal 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);
|
118
app/kubernetes/models/application/formValues.js
Normal file
118
app/kubernetes/models/application/formValues.js
Normal 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)));
|
||||
}
|
||||
}
|
114
app/kubernetes/models/application/models.js
Normal file
114
app/kubernetes/models/application/models.js
Normal 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)));
|
||||
}
|
||||
}
|
142
app/kubernetes/models/application/payloads.js
Normal file
142
app/kubernetes/models/application/payloads.js
Normal 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)));
|
||||
}
|
||||
}
|
11
app/kubernetes/models/common/params.js
Normal file
11
app/kubernetes/models/common/params.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Generic params
|
||||
*/
|
||||
const _KubernetesCommonParams = Object.freeze({
|
||||
id: '',
|
||||
});
|
||||
export class KubernetesCommonParams {
|
||||
constructor() {
|
||||
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesCommonParams)));
|
||||
}
|
||||
}
|
15
app/kubernetes/models/common/payloads.js
Normal file
15
app/kubernetes/models/common/payloads.js
Normal 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
Loading…
Add table
Add a link
Reference in a new issue