mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +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
60
app/kubernetes/views/applications/applications.html
Normal file
60
app/kubernetes/views/applications/applications.html
Normal file
|
@ -0,0 +1,60 @@
|
|||
<kubernetes-view-header title="Application list" state="kubernetes.applications" view-ready="ctrl.state.viewReady">
|
||||
Applications
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<information-panel title-text="Advanced deployment" ng-if="ctrl.state.isAdmin">
|
||||
<span class="small">
|
||||
<p class="text-muted">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
As an administrator user, you have access to the advanced deployment feature allowing you to deploy any Kubernetes manifest inside your cluster.
|
||||
</p>
|
||||
<p>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.deploy">
|
||||
<i class="fa fa-file-code space-right" aria-hidden="true"></i>Advanced deployment
|
||||
</button>
|
||||
</p>
|
||||
</span>
|
||||
</information-panel>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<uib-tabset active="ctrl.state.activeTab" justified="true" type="pills">
|
||||
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)">
|
||||
<uib-tab-heading> <i class="fa fa-laptop-code space-right" aria-hidden="true"></i> Applications </uib-tab-heading>
|
||||
<kubernetes-applications-datatable
|
||||
dataset="ctrl.applications"
|
||||
table-key="kubernetes.applications"
|
||||
order-by="Name"
|
||||
remove-action="ctrl.removeAction"
|
||||
refresh-callback="ctrl.getApplications"
|
||||
on-publishing-mode-click="(ctrl.onPublishingModeClick)"
|
||||
>
|
||||
</kubernetes-applications-datatable>
|
||||
</uib-tab>
|
||||
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)">
|
||||
<uib-tab-heading> <i class="fa fa-exchange-alt space-right" aria-hidden="true"></i> Port mappings </uib-tab-heading>
|
||||
<kubernetes-applications-ports-datatable dataset="ctrl.ports" table-key="kubernetes.applications.ports" order-by="Name" refresh-callback="ctrl.getApplications">
|
||||
</kubernetes-applications-ports-datatable>
|
||||
</uib-tab>
|
||||
<uib-tab index="2" classes="btn-sm" select="ctrl.selectTab(2)">
|
||||
<uib-tab-heading> <i class="fa fa-th-list space-right" aria-hidden="true"></i> Stacks </uib-tab-heading>
|
||||
<kubernetes-applications-stacks-datatable
|
||||
dataset="ctrl.stacks"
|
||||
table-key="kubernetes.applications.stacks"
|
||||
order-by="Name"
|
||||
refresh-callback="ctrl.getApplications"
|
||||
remove-action="ctrl.removeStacksAction"
|
||||
>
|
||||
</kubernetes-applications-stacks-datatable>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
5
app/kubernetes/views/applications/applications.js
Normal file
5
app/kubernetes/views/applications/applications.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesApplicationsView', {
|
||||
templateUrl: './applications.html',
|
||||
controller: 'KubernetesApplicationsController',
|
||||
controllerAs: 'ctrl',
|
||||
});
|
139
app/kubernetes/views/applications/applicationsController.js
Normal file
139
app/kubernetes/views/applications/applicationsController.js
Normal file
|
@ -0,0 +1,139 @@
|
|||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import KubernetesStackHelper from 'Kubernetes/helpers/stackHelper';
|
||||
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
|
||||
|
||||
class KubernetesApplicationsController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, Notifications, KubernetesApplicationService, Authentication, ModalService, LocalStorage) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.Notifications = Notifications;
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
this.Authentication = Authentication;
|
||||
this.ModalService = ModalService;
|
||||
this.LocalStorage = LocalStorage;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.getApplications = this.getApplications.bind(this);
|
||||
this.getApplicationsAsync = this.getApplicationsAsync.bind(this);
|
||||
this.removeAction = this.removeAction.bind(this);
|
||||
this.removeActionAsync = this.removeActionAsync.bind(this);
|
||||
this.removeStacksAction = this.removeStacksAction.bind(this);
|
||||
this.removeStacksActionAsync = this.removeStacksActionAsync.bind(this);
|
||||
this.onPublishingModeClick = this.onPublishingModeClick.bind(this);
|
||||
}
|
||||
|
||||
selectTab(index) {
|
||||
this.LocalStorage.storeActiveTab('applications', index);
|
||||
}
|
||||
|
||||
async removeStacksActionAsync(selectedItems) {
|
||||
let actionCount = selectedItems.length;
|
||||
for (const stack of selectedItems) {
|
||||
try {
|
||||
const promises = _.map(stack.Applications, (app) => this.KubernetesApplicationService.delete(app));
|
||||
await Promise.all(promises);
|
||||
this.Notifications.success('Stack successfully removed', stack.Name);
|
||||
_.remove(this.stacks, { Name: stack.Name });
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to remove stack');
|
||||
} finally {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
this.$state.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeStacksAction(selectedItems) {
|
||||
this.ModalService.confirmDeletion(
|
||||
'Are you sure that you want to remove the selected stack(s) ? This will remove all the applications associated to the stack(s).',
|
||||
(confirmed) => {
|
||||
if (confirmed) {
|
||||
return this.$async(this.removeStacksActionAsync, selectedItems);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async removeActionAsync(selectedItems) {
|
||||
let actionCount = selectedItems.length;
|
||||
for (const application of selectedItems) {
|
||||
try {
|
||||
await this.KubernetesApplicationService.delete(application);
|
||||
this.Notifications.success('Application successfully removed', application.Name);
|
||||
const index = this.applications.indexOf(application);
|
||||
this.applications.splice(index, 1);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to remove application');
|
||||
} finally {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
this.$state.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeAction(selectedItems) {
|
||||
return this.$async(this.removeActionAsync, selectedItems);
|
||||
}
|
||||
|
||||
onPublishingModeClick(application) {
|
||||
this.state.activeTab = 1;
|
||||
_.forEach(this.ports, (item) => {
|
||||
item.Expanded = false;
|
||||
item.Highlighted = false;
|
||||
if (item.Name === application.Name) {
|
||||
if (item.Ports.length > 1) {
|
||||
item.Expanded = true;
|
||||
}
|
||||
item.Highlighted = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getApplicationsAsync() {
|
||||
try {
|
||||
this.applications = await this.KubernetesApplicationService.get();
|
||||
this.stacks = KubernetesStackHelper.stacksFromApplications(this.applications);
|
||||
this.ports = KubernetesApplicationHelper.portMappingsFromApplications(this.applications);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve applications');
|
||||
}
|
||||
}
|
||||
|
||||
getApplications() {
|
||||
return this.$async(this.getApplicationsAsync);
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
this.state = {
|
||||
activeTab: 0,
|
||||
currentName: this.$state.$current.name,
|
||||
isAdmin: this.Authentication.isAdmin(),
|
||||
viewReady: false,
|
||||
};
|
||||
|
||||
this.state.activeTab = this.LocalStorage.getActiveTab('applications');
|
||||
|
||||
await this.getApplications();
|
||||
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
if (this.state.currentName !== this.$state.$current.name) {
|
||||
this.LocalStorage.storeActiveTab('applications', 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesApplicationsController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesApplicationsController', KubernetesApplicationsController);
|
71
app/kubernetes/views/applications/console/console.html
Normal file
71
app/kubernetes/views/applications/console/console.html
Normal file
|
@ -0,0 +1,71 @@
|
|||
<kubernetes-view-header title="Application console" state="kubernetes.applications.application.console" view-ready="ctrl.state.viewReady">
|
||||
<a ui-sref="kubernetes.resourcePools">Resource pools</a> >
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.application.ResourcePool })">{{ ctrl.application.ResourcePool }}</a> >
|
||||
<a ui-sref="kubernetes.applications">Applications</a> >
|
||||
<a ui-sref="kubernetes.applications.application({ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool })">{{ ctrl.application.Name }}</a> > Pods >
|
||||
{{ ctrl.podName }} > Console
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" autocomplete="off">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Console
|
||||
</div>
|
||||
<!-- Command -->
|
||||
<div class="form-group">
|
||||
<label for="console_command" class="col-sm-1 control-label text-left">Command</label>
|
||||
<div class="col-sm-10 input-group">
|
||||
<span class="input-group-addon">
|
||||
<i class="fas fa-terminal" aria-hidden="true"></i>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="/bin/bash"
|
||||
ng-model="ctrl.state.command"
|
||||
name="console_command"
|
||||
uib-typeahead="command for command in ctrl.state.availableCommands | filter:$viewValue | limitTo:5"
|
||||
typeahead-show-hint="true"
|
||||
typeahead-min-length="0"
|
||||
auto-focus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !command -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
style="margin: 0;"
|
||||
ng-if="!ctrl.state.connected"
|
||||
ng-disabled="!ctrl.state.command || ctrl.state.connected"
|
||||
ng-click="ctrl.connectConsole()"
|
||||
button-spinner="ctrl.state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="ctrl.state.actionInProgress">Connect</span>
|
||||
<span ng-show="ctrl.state.actionInProgress">Connection in progress...</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" style="margin: 0;" ng-if="ctrl.state.connected" ng-click="ctrl.disconnect()">
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div id="terminal-container" class="terminal-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
8
app/kubernetes/views/applications/console/console.js
Normal file
8
app/kubernetes/views/applications/console/console.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesApplicationConsoleView', {
|
||||
templateUrl: './console.html',
|
||||
controller: 'KubernetesApplicationConsoleController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
},
|
||||
});
|
113
app/kubernetes/views/applications/console/consoleController.js
Normal file
113
app/kubernetes/views/applications/console/consoleController.js
Normal file
|
@ -0,0 +1,113 @@
|
|||
import angular from 'angular';
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
class KubernetesApplicationConsoleController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, Notifications, KubernetesApplicationService, EndpointProvider, LocalStorage) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.Notifications = Notifications;
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
this.EndpointProvider = EndpointProvider;
|
||||
this.LocalStorage = LocalStorage;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.state.socket.close();
|
||||
this.state.term.dispose();
|
||||
this.state.connected = false;
|
||||
}
|
||||
|
||||
configureSocketAndTerminal(socket, term) {
|
||||
socket.onopen = function () {
|
||||
const terminal_container = document.getElementById('terminal-container');
|
||||
term.open(terminal_container);
|
||||
term.setOption('cursorBlink', true);
|
||||
term.focus();
|
||||
};
|
||||
|
||||
term.on('data', function (data) {
|
||||
socket.send(data);
|
||||
});
|
||||
|
||||
socket.onmessage = function (msg) {
|
||||
term.write(msg.data);
|
||||
};
|
||||
|
||||
socket.onerror = function (err) {
|
||||
this.disconnect();
|
||||
this.Notifications.error('Failure', err, 'Websocket connection error');
|
||||
}.bind(this);
|
||||
|
||||
this.state.socket.onclose = function () {
|
||||
this.disconnect();
|
||||
}.bind(this);
|
||||
|
||||
this.state.connected = true;
|
||||
}
|
||||
|
||||
connectConsole() {
|
||||
const params = {
|
||||
token: this.LocalStorage.getJWT(),
|
||||
endpointId: this.EndpointProvider.endpointID(),
|
||||
namespace: this.application.ResourcePool,
|
||||
podName: this.podName,
|
||||
containerName: this.application.Pods[0].Containers[0].name,
|
||||
command: this.state.command,
|
||||
};
|
||||
|
||||
let url =
|
||||
window.location.href.split('#')[0] +
|
||||
'api/websocket/pod?' +
|
||||
Object.keys(params)
|
||||
.map((k) => k + '=' + params[k])
|
||||
.join('&');
|
||||
if (url.indexOf('https') > -1) {
|
||||
url = url.replace('https://', 'wss://');
|
||||
} else {
|
||||
url = url.replace('http://', 'ws://');
|
||||
}
|
||||
|
||||
this.state.socket = new WebSocket(url);
|
||||
this.state.term = new Terminal();
|
||||
|
||||
this.configureSocketAndTerminal(this.state.socket, this.state.term);
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
const availableCommands = ['/bin/bash', '/bin/sh'];
|
||||
|
||||
this.state = {
|
||||
actionInProgress: false,
|
||||
availableCommands: availableCommands,
|
||||
command: availableCommands[1],
|
||||
connected: false,
|
||||
socket: null,
|
||||
term: null,
|
||||
viewReady: false,
|
||||
};
|
||||
|
||||
const podName = this.$transition$.params().pod;
|
||||
const applicationName = this.$transition$.params().name;
|
||||
const namespace = this.$transition$.params().namespace;
|
||||
|
||||
this.podName = podName;
|
||||
|
||||
try {
|
||||
this.application = await this.KubernetesApplicationService.get(namespace, applicationName);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve application logs');
|
||||
} finally {
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesApplicationConsoleController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesApplicationConsoleController', KubernetesApplicationConsoleController);
|
904
app/kubernetes/views/applications/create/createApplication.html
Normal file
904
app/kubernetes/views/applications/create/createApplication.html
Normal file
|
@ -0,0 +1,904 @@
|
|||
<kubernetes-view-header ng-if="!ctrl.state.isEdit" title="Create application" state="kubernetes.applications.new" view-ready="ctrl.state.viewReady">
|
||||
<a ui-sref="kubernetes.applications">Applications</a> > Create an application
|
||||
</kubernetes-view-header>
|
||||
<kubernetes-view-header ng-if="ctrl.state.isEdit" title="Edit application" state="kubernetes.applications.application.edit" view-ready="ctrl.state.viewReady">
|
||||
<a ui-sref="kubernetes.resourcePools">Resource pools</a> >
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.application.ResourcePool })">{{ ctrl.application.ResourcePool }}</a> >
|
||||
<a ui-sref="kubernetes.applications">Applications</a> >
|
||||
<a ui-sref="kubernetes.applications.application({ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool })">{{ ctrl.application.Name }}</a> > Edit
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="kubernetesApplicationCreationForm" autocomplete="off">
|
||||
<!-- name -->
|
||||
<div class="form-group">
|
||||
<label for="application_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="application_name"
|
||||
ng-model="ctrl.formValues.Name"
|
||||
ng-change="ctrl.onChangeName()"
|
||||
placeholder="my-app"
|
||||
ng-pattern="/^[a-z]([-a-z0-9]*[a-z0-9])?$/"
|
||||
auto-focus
|
||||
required
|
||||
ng-disabled="ctrl.state.isEdit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="kubernetesApplicationCreationForm.application_name.$invalid || ctrl.state.alreadyExists">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="kubernetesApplicationCreationForm.application_name.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
<p ng-message="pattern"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist of lower case alphanumeric characters or '-', start with an alphabetic
|
||||
character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123').</p
|
||||
>
|
||||
</div>
|
||||
<p ng-if="ctrl.state.alreadyExists"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> An application with the same name already exists inside the selected resource pool.</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name -->
|
||||
|
||||
<!-- image -->
|
||||
<div class="form-group">
|
||||
<label for="container_image" class="col-sm-1 control-label text-left">Image</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" name="container_image" ng-model="ctrl.formValues.Image" placeholder="nginx:latest" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="kubernetesApplicationCreationForm.container_image.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="kubernetesApplicationCreationForm.container_image.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !image -->
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Resource pool
|
||||
</div>
|
||||
|
||||
<!-- resource-pool -->
|
||||
<div class="form-group">
|
||||
<label for="resource-pool-selector" class="col-sm-1 control-label text-left">Resource pool</label>
|
||||
<div class="col-sm-11">
|
||||
<select
|
||||
class="form-control"
|
||||
id="resource-pool-selector"
|
||||
ng-model="ctrl.formValues.ResourcePool"
|
||||
ng-options="resourcePool.Namespace.Name for resourcePool in ctrl.resourcePools"
|
||||
ng-change="ctrl.onResourcePoolSelectionChange()"
|
||||
ng-disabled="ctrl.state.isEdit"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded()">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This resource pool has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the
|
||||
resource pool.
|
||||
</div>
|
||||
</div>
|
||||
<!-- !resource-pool -->
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Stack
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Portainer can automatically bundle multiple applications inside a stack. Enter a name of a new stack or select an existing stack in the list. Leave empty to use the
|
||||
application name.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- stack -->
|
||||
<div class="form-group">
|
||||
<label for="stack_name" class="col-sm-1 control-label text-left">Stack</label>
|
||||
<div class="col-sm-11">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="myStack"
|
||||
ng-model="ctrl.formValues.StackName"
|
||||
name="stack_name"
|
||||
uib-typeahead="stack for stack in ctrl.stacks | filter:$viewValue | limitTo:7"
|
||||
typeahead-show-hint="true"
|
||||
typeahead-min-length="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !stack -->
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Environment
|
||||
</div>
|
||||
|
||||
<!-- environment-variables -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">Environment variables</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addEnvironmentVariable()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add environment variable
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="envVar in ctrl.formValues.EnvironmentVariables" style="margin-top: 2px;">
|
||||
<div class="col-sm-4 input-group input-group-sm" style="vertical-align: top;">
|
||||
<div class="input-group col-sm-12 input-group-sm" ng-class="{ striked: envVar.NeedsDeletion }">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input
|
||||
type="text"
|
||||
name="environment_variable_name_{{ $index }}"
|
||||
class="form-control"
|
||||
ng-model="envVar.Name"
|
||||
ng-change="ctrl.onChangeEnvironmentName($index)"
|
||||
placeholder="foo"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="small text-warning"
|
||||
style="margin-top: 5px;"
|
||||
ng-show="kubernetesApplicationCreationForm['environment_variable_name_' + $index].$invalid || ctrl.state.duplicateEnvironmentVariables[$index] !== undefined"
|
||||
>
|
||||
<ng-messages for="kubernetesApplicationCreationForm['environment_variable_name_' + $index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Environment variable name is required.</p>
|
||||
</ng-messages>
|
||||
<p ng-if="ctrl.state.duplicateEnvironmentVariables[$index] !== undefined"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This environment variable is already defined.</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group col-sm-4 input-group-sm" ng-class="{ striked: envVar.NeedsDeletion }">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" name="environment_variable_value_{{ $index }}" class="form-control" ng-model="envVar.Value" placeholder="bar" />
|
||||
</div>
|
||||
|
||||
<div class="input-group col-sm-2 input-group-sm">
|
||||
<button ng-if="!envVar.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeEnvironmentVariable($index)">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button ng-if="envVar.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restoreEnvironmentVariable($index)">
|
||||
Restore
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !environment-variables -->
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Configurations
|
||||
</div>
|
||||
|
||||
<!-- configurations -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">Configurations</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addConfiguration()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add configuration
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 small text-muted" style="margin-top: 15px;" ng-if="ctrl.formValues.Configurations.length">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Portainer will automatically expose all the keys of a configuration as environment variables. This behavior can be overriden to filesystem mounts for each key via
|
||||
the override button.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- config-element -->
|
||||
<div class="form-group" ng-repeat="(index, config) in ctrl.formValues.Configurations">
|
||||
<label for="stack_name" class="col-md-1 col-sm-2 control-label text-left">Configuration</label>
|
||||
<div class="col-sm-5">
|
||||
<select
|
||||
class="form-control"
|
||||
ng-model="config.SelectedConfiguration"
|
||||
ng-options="c as c.Name for c in ctrl.configurations"
|
||||
ng-change="ctrl.resetConfiguration(index)"
|
||||
></select>
|
||||
</div>
|
||||
<div class="col-sm-6" style="margin-top: 2px;">
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
type="button"
|
||||
ng-if="!config.Overriden"
|
||||
ng-click="ctrl.overrideConfiguration(index)"
|
||||
ng-disabled="!config.SelectedConfiguration"
|
||||
>
|
||||
<i class="fa fa-list" aria-hidden="true"></i> Override
|
||||
</button>
|
||||
<button class="btn btn-sm btn-primary" type="button" ng-if="config.Overriden" ng-click="ctrl.resetConfiguration(index)">
|
||||
<i class="fa fa-undo" aria-hidden="true"></i> Auto
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeConfiguration(index)"> <i class="fa fa-trash" aria-hidden="true"></i> Remove </button>
|
||||
</div>
|
||||
<!-- no-override -->
|
||||
<div class="col-sm-12" style="margin-top: 10px;" ng-if="config.SelectedConfiguration && !config.Overriden">
|
||||
<div class="col-md-1 col-sm-2"></div>
|
||||
<div class="col-md-11 col-sm-10 small text-muted" style="padding-left: 5px;">
|
||||
The following keys will be loaded from the <code>{{ config.SelectedConfiguration.Name }}</code> configuration as environment variables:
|
||||
<span ng-repeat="(key, _) in config.SelectedConfiguration.Data">
|
||||
<code>{{ key }}</code
|
||||
>{{ $last ? '' : ', ' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !no-override -->
|
||||
|
||||
<!-- has-override -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;" ng-if="config.Overriden">
|
||||
<div ng-repeat="(keyIndex, overridenKey) in config.OverridenKeys" style="margin-top: 2px;">
|
||||
<div class="col-md-1 col-sm-2" style="margin-left: 3px;" style="vertical-align: top;"></div>
|
||||
<div class="input-group col-sm-3 input-group-sm" style="vertical-align: top;">
|
||||
<span class="input-group-addon">configuration key</span>
|
||||
<input type="text" class="form-control" ng-value="overridenKey.Key" disabled />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col-sm-3 input-group input-group-sm"
|
||||
style="vertical-align: top;"
|
||||
ng-if="overridenKey.Type === ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM"
|
||||
>
|
||||
<div class="input-group col-sm-12 input-group-sm">
|
||||
<span class="input-group-addon">path on disk</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="overridenKey.Path"
|
||||
placeholder="/etc/myapp/conf.d"
|
||||
name="overriden_key_path_{{ index }}_{{ keyIndex }}"
|
||||
required
|
||||
ng-change="ctrl.onChangeConfigurationPath()"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="small text-warning"
|
||||
style="margin-top: 5px;"
|
||||
ng-show="
|
||||
kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$invalid ||
|
||||
ctrl.state.duplicateConfigurationPaths[index + '_' + keyIndex] !== undefined
|
||||
"
|
||||
>
|
||||
<ng-messages for="kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Path is required.</p>
|
||||
</ng-messages>
|
||||
<p ng-if="ctrl.state.duplicateConfigurationPaths[index + '_' + keyIndex] !== undefined"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This path is already used.</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group col-sm-3 btn-group btn-group-sm" style="vertical-align: top;">
|
||||
<label class="btn btn-primary" ng-model="overridenKey.Type" uib-btn-radio="ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT">
|
||||
<i class="fa fa-list" aria-hidden="true"></i> Environment
|
||||
</label>
|
||||
<label class="btn btn-primary" ng-model="overridenKey.Type" uib-btn-radio="ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM">
|
||||
<i class="fa fa-file" aria-hidden="true"></i> Filesystem
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !has-override -->
|
||||
</div>
|
||||
<!-- !config-element -->
|
||||
<!-- !configurations -->
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Persisting data
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="!ctrl.storageClassAvailable()">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
No storage option is available to persist data, contact your administrator to enable a storage option.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- persisted folders -->
|
||||
<div class="form-group" ng-if="ctrl.storageClassAvailable()">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Persisted folders</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addPersistedFolder()" ng-if="!ctrl.isEditAndStatefulSet()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add persisted folder
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat-start="persistedFolder in ctrl.formValues.PersistedFolders" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-3 input-group-sm" ng-class="{ striked: persistedFolder.NeedsDeletion }">
|
||||
<span class="input-group-addon">path in container</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="persisted_folder_path_{{ $index }}"
|
||||
ng-model="persistedFolder.ContainerPath"
|
||||
ng-change="ctrl.onChangePersistedFolderPath($index)"
|
||||
placeholder="/data"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="input-group col-sm-3 input-group-sm" ng-class="{ striked: persistedFolder.NeedsDeletion }">
|
||||
<span class="input-group-addon">requested size</span>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
name="persisted_folder_size_{{ $index }}"
|
||||
ng-model="persistedFolder.Size"
|
||||
placeholder="20"
|
||||
ng-min="0"
|
||||
required
|
||||
ng-disabled="ctrl.isEditAndExistingPersistedFolder($index)"
|
||||
/>
|
||||
<span class="input-group-addon" style="padding: 0;">
|
||||
<select
|
||||
ng-model="persistedFolder.SizeUnit"
|
||||
ng-style="{ width: '100%', height: '100%', cursor: ctrl.isEditAndExistingPersistedFolder($index) ? 'not-allowed' : 'auto' }"
|
||||
ng-options="unit for unit in ctrl.state.availableSizeUnits"
|
||||
ng-disabled="ctrl.isEditAndExistingPersistedFolder($index)"
|
||||
></select>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="input-group col-sm-3 input-group-sm" ng-class="{ striked: persistedFolder.NeedsDeletion }" style="vertical-align: top;">
|
||||
<span class="input-group-addon">storage</span>
|
||||
<select
|
||||
ng-if="ctrl.hasMultipleStorageClassesAvailable()"
|
||||
class="form-control"
|
||||
ng-model="persistedFolder.StorageClass"
|
||||
ng-options="storageClass as storageClass.Name for storageClass in ctrl.storageClasses"
|
||||
ng-disabled="ctrl.state.isEdit"
|
||||
></select>
|
||||
<input ng-if="!ctrl.hasMultipleStorageClassesAvailable()" type="text" class="form-control" disabled ng-model="persistedFolder.StorageClass.Name" />
|
||||
</div>
|
||||
|
||||
<div class="input-group col-sm-1 input-group-sm" style="vertical-align: top;" ng-if="!ctrl.isEditAndStatefulSet()">
|
||||
<button ng-if="!persistedFolder.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removePersistedFolder($index)">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button ng-if="persistedFolder.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restorePersistedFolder($index)">
|
||||
Restore
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ng-repeat-end
|
||||
ng-show="
|
||||
kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$invalid ||
|
||||
ctrl.state.duplicatePersistedFolderPaths[$index] !== undefined ||
|
||||
kubernetesApplicationCreationForm['persisted_folder_size_' + $index].$invalid
|
||||
"
|
||||
>
|
||||
<div class="input-group col-sm-3 input-group-sm">
|
||||
<div
|
||||
class="small text-warning"
|
||||
style="margin-top: 5px;"
|
||||
ng-show="kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$invalid || ctrl.state.duplicatePersistedFolderPaths[$index] !== undefined"
|
||||
>
|
||||
<ng-messages for="kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Path is required.</p>
|
||||
</ng-messages>
|
||||
<p ng-if="ctrl.state.duplicatePersistedFolderPaths[$index] !== undefined"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This path is already defined.</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group col-sm-3 input-group-sm">
|
||||
<div class="small text-warning" style="margin-top: 5px;" ng-show="kubernetesApplicationCreationForm['persisted_folder_size_' + $index].$invalid">
|
||||
<ng-messages for="kubernetesApplicationCreationForm['persisted_folder_size_' + $index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Size is required.</p>
|
||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This value must be greater than zero.</p>
|
||||
</ng-messages>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group col-sm-3 input-group-sm"> </div>
|
||||
|
||||
<div class="input-group col-sm-1 input-group-sm"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !persisted folders -->
|
||||
|
||||
<div ng-if="ctrl.showDataAccessPolicySection()">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">Data access policy</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
Specify how the data will be used across instances.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- access policy options -->
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<div class="boxselector_wrapper">
|
||||
<div ng-if="!ctrl.state.isEdit || (ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED)">
|
||||
<input
|
||||
type="radio"
|
||||
id="data_access_shared"
|
||||
ng-value="ctrl.ApplicationDataAccessPolicies.SHARED"
|
||||
ng-model="ctrl.formValues.DataAccessPolicy"
|
||||
ng-change="ctrl.resetDeploymentType()"
|
||||
/>
|
||||
<label for="data_access_shared">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-cube" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Shared
|
||||
</div>
|
||||
<p>All the instances of this application will use the same data</p>
|
||||
</label>
|
||||
</div>
|
||||
<div style="color: #767676;" ng-if="ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.ISOLATED">
|
||||
<input type="radio" id="data_access_shared" disabled />
|
||||
<label
|
||||
for="data_access_shared"
|
||||
tooltip-append-to-body="true"
|
||||
tooltip-placement="bottom"
|
||||
tooltip-class="portainer-tooltip"
|
||||
uib-tooltip="Changing the data access policy is not allowed"
|
||||
style="cursor: pointer; border-color: #767676;"
|
||||
>
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-cube" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Shared
|
||||
</div>
|
||||
<p>All the instances of this application will use the same data</p>
|
||||
</label>
|
||||
</div>
|
||||
<div ng-if="!ctrl.state.isEdit || (ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.ISOLATED)">
|
||||
<input
|
||||
type="radio"
|
||||
id="data_access_isolated"
|
||||
ng-value="ctrl.ApplicationDataAccessPolicies.ISOLATED"
|
||||
ng-model="ctrl.formValues.DataAccessPolicy"
|
||||
ng-change="ctrl.resetDeploymentType()"
|
||||
/>
|
||||
<label for="data_access_isolated">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-cubes" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Isolated
|
||||
</div>
|
||||
<p>Every instance of this application will use their own data</p>
|
||||
</label>
|
||||
</div>
|
||||
<div style="color: #767676;" ng-if="ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED">
|
||||
<input type="radio" id="data_access_isolated" disabled />
|
||||
<label
|
||||
for="data_access_isolated"
|
||||
tooltip-append-to-body="true"
|
||||
tooltip-placement="bottom"
|
||||
tooltip-class="portainer-tooltip"
|
||||
uib-tooltip="Changing the data access policy is not allowed"
|
||||
style="cursor: pointer; border-color: #767676;"
|
||||
>
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-cubes" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Isolated
|
||||
</div>
|
||||
<p>Every instance of this application will use their own data</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !access policy options -->
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Resource reservations
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="!ctrl.state.resourcePoolHasQuota">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Resource reservations are applied per instance of the application.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && !ctrl.resourceQuotaCapacityExceeded()">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
A resource quota is set on this resource pool, you must specify resource reservations. Resource reservations are applied per instance of the application. Maximums
|
||||
are inherited from the resource pool quota.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded()">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This resource pool has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the
|
||||
resource pool.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- memory-limit-input -->
|
||||
<div class="form-group" ng-if="!ctrl.state.resourcePoolHasQuota || (ctrl.state.resourcePoolHasQuota && !ctrl.resourceQuotaCapacityExceeded())">
|
||||
<label for="memory-limit" class="col-sm-3 col-lg-2 control-label text-left" style="margin-top: 20px;">
|
||||
Memory
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="An instance of this application will reserve this amount of memory. If the instance memory usage exceeds the reservation, it might be subject to OOM."
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-3">
|
||||
<slider model="ctrl.formValues.MemoryLimit" floor="ctrl.state.sliders.memory.min" ceil="ctrl.state.sliders.memory.max" step="128"></slider>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<input
|
||||
name="memory_limit"
|
||||
ng-model="ctrl.formValues.MemoryLimit"
|
||||
type="number"
|
||||
min="{{ ctrl.state.sliders.memory.min }}"
|
||||
max="{{ ctrl.state.sliders.memory.max }}"
|
||||
class="form-control"
|
||||
id="memory-limit"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<p class="small text-muted" style="margin-top: 7px;">
|
||||
Maximum memory usage (<b>MB</b>)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="kubernetesApplicationCreationForm.memory_limit.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="kubernetesApplicationCreationForm.memory_limit.$error">
|
||||
<p
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Value must be between {{ ctrl.state.sliders.memory.min }} and {{ ctrl.state.sliders.memory.max }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !memory-limit-input -->
|
||||
<!-- cpu-limit-input -->
|
||||
<div class="form-group" ng-if="!ctrl.state.resourcePoolHasQuota || (ctrl.state.resourcePoolHasQuota && !ctrl.resourceQuotaCapacityExceeded())">
|
||||
<label for="cpu-limit" class="col-sm-3 col-lg-2 control-label text-left" style="margin-top: 20px;">
|
||||
CPU
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="An instance of this application will reserve this amount of CPU. If the instance CPU usage exceeds the reservation, it might be subject to CPU throttling."
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
<slider model="ctrl.formValues.CpuLimit" floor="ctrl.state.sliders.cpu.min" ceil="ctrl.state.sliders.cpu.max" step="0.10" precision="2"></slider>
|
||||
</div>
|
||||
<div class="col-sm-4" style="margin-top: 20px;">
|
||||
<p class="small text-muted">
|
||||
Maximum CPU usage
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !cpu-limit-input -->
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Deployment
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
Select how you want to deploy your application inside the cluster.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- deployment options -->
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<div class="boxselector_wrapper">
|
||||
<div>
|
||||
<input type="radio" id="deployment_replicated" ng-value="ctrl.ApplicationDeploymentTypes.REPLICATED" ng-model="ctrl.formValues.DeploymentType" />
|
||||
<label for="deployment_replicated">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-cube" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Replicated
|
||||
</div>
|
||||
<p>Run one or multiple instances of this container</p>
|
||||
</label>
|
||||
</div>
|
||||
<div style="color: #767676;" ng-if="!ctrl.supportGlobalDeployment()">
|
||||
<input type="radio" id="deployment_global" disabled />
|
||||
<label
|
||||
for="deployment_global"
|
||||
tooltip-append-to-body="true"
|
||||
tooltip-placement="bottom"
|
||||
tooltip-class="portainer-tooltip"
|
||||
uib-tooltip="The storage or access policy used for persisted folders cannot be used with this option"
|
||||
style="cursor: pointer; border-color: #767676;"
|
||||
>
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-cubes" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Global
|
||||
</div>
|
||||
<p>Deploy an instance of this container on each node of the cluster</p>
|
||||
</label>
|
||||
</div>
|
||||
<div ng-if="ctrl.supportGlobalDeployment()">
|
||||
<input type="radio" id="deployment_global" ng-value="ctrl.ApplicationDeploymentTypes.GLOBAL" ng-model="ctrl.formValues.DeploymentType" />
|
||||
<label for="deployment_global">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-cubes" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Global
|
||||
</div>
|
||||
<p>Deploy an instance of this container on each node of the cluster</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !deployment options -->
|
||||
|
||||
<!-- replica count -->
|
||||
<div class="form-group form-inline" ng-if="ctrl.formValues.DeploymentType === ctrl.ApplicationDeploymentTypes.REPLICATED">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Instance count
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="9999"
|
||||
placeholder="1"
|
||||
style="margin-left: 20px;"
|
||||
ng-model="ctrl.formValues.ReplicaCount"
|
||||
ng-disabled="!ctrl.supportScalableReplicaDeployment()"
|
||||
ng-change="ctrl.enforceReplicaCountMinimum()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !replica count -->
|
||||
|
||||
<div
|
||||
class="form-group"
|
||||
ng-if="!ctrl.resourceReservationsOverflow() && ctrl.formValues.ReplicaCount > 1 && (ctrl.formValues.CpuLimit !== 0 || ctrl.formValues.MemoryLimit !== 0)"
|
||||
>
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This application will reserve the following resources: <b>{{ ctrl.formValues.CpuLimit * ctrl.formValues.ReplicaCount | kubernetesApplicationCPUValue }} CPU</b> and
|
||||
<b>{{ ctrl.formValues.MemoryLimit * ctrl.formValues.ReplicaCount }} MB</b> of memory.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="ctrl.resourceReservationsOverflow()">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This application would exceed available resources. Please review resource reservations or the instance count.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="!ctrl.supportScalableReplicaDeployment()">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
The following storage option(s) do not support concurrent access from multiples instances: <code>{{ ctrl.getNonScalableStorage() }}</code
|
||||
>. You will not be able to scale that application.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Publishing the application
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
Select how you want to publish your application.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- publishing options -->
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<div class="boxselector_wrapper">
|
||||
<div>
|
||||
<input type="radio" id="publishing_internal" ng-value="ctrl.ApplicationPublishingTypes.INTERNAL" ng-model="ctrl.formValues.PublishingType" />
|
||||
<label for="publishing_internal">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-list-alt" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Internal
|
||||
</div>
|
||||
<p>Internal communications inside the cluster only</p>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="publishing_cluster" ng-value="ctrl.ApplicationPublishingTypes.CLUSTER" ng-model="ctrl.formValues.PublishingType" />
|
||||
<label for="publishing_cluster">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-list" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Cluster
|
||||
</div>
|
||||
<p>Publish this application via a port on all nodes of the cluster</p>
|
||||
</label>
|
||||
</div>
|
||||
<div ng-if="ctrl.publishViaLoadBalancerEnabled()">
|
||||
<input type="radio" id="publishing_loadbalancer" ng-value="ctrl.ApplicationPublishingTypes.LOAD_BALANCER" ng-model="ctrl.formValues.PublishingType" />
|
||||
<label for="publishing_loadbalancer">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-project-diagram" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Load balancer
|
||||
</div>
|
||||
<p>Publish this application via a load balancer</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !publishing options -->
|
||||
|
||||
<!-- published ports -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Published ports</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addPublishedPort()" ng-if="!ctrl.disableLoadBalancerEdit()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> publish a new port
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col-sm-12 small text-muted"
|
||||
style="margin-top: 15px;"
|
||||
ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER && ctrl.formValues.PublishedPorts.length > 0"
|
||||
>
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
When publishing a port in cluster mode, the node port is optional. If left empty Kubernetes will use a random port number. If you wish to specify a port, use a port
|
||||
number inside the default range <code>30000-32767</code>.
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div
|
||||
ng-repeat-start="publishedPort in ctrl.formValues.PublishedPorts"
|
||||
style="margin-top: 2px;"
|
||||
tooltip-append-to-body="true"
|
||||
tooltip-placement="bottom"
|
||||
tooltip-class="portainer-tooltip"
|
||||
tooltip-enable="ctrl.disableLoadBalancerEdit()"
|
||||
uib-tooltip="Edition is not allowed while the Load Balancer is in Pending state"
|
||||
>
|
||||
<div class="col-sm-4 input-group input-group-sm">
|
||||
<span class="input-group-addon">container port</span>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
name="container_port_{{ $index }}"
|
||||
ng-model="publishedPort.ContainerPort"
|
||||
placeholder="80"
|
||||
ng-min="1"
|
||||
ng-max="65535"
|
||||
required
|
||||
ng-disabled="ctrl.disableLoadBalancerEdit()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-sm col-sm-4" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER">
|
||||
<span class="input-group-addon">node port</span>
|
||||
<input
|
||||
name="published_node_port_{{ $index }}"
|
||||
type="number"
|
||||
class="form-control"
|
||||
ng-model="publishedPort.NodePort"
|
||||
placeholder="30080"
|
||||
ng-min="30000"
|
||||
ng-max="32767"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4 input-group input-group-sm" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.LOAD_BALANCER">
|
||||
<span class="input-group-addon">load balancer port</span>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
name="load_balancer_port_{{ $index }}"
|
||||
ng-model="publishedPort.LoadBalancerPort"
|
||||
placeholder="80"
|
||||
value="8080"
|
||||
ng-min="1"
|
||||
ng-max="65535"
|
||||
required
|
||||
ng-disabled="ctrl.disableLoadBalancerEdit()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="input-group col-sm-3 input-group-sm">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<label class="btn btn-primary" ng-model="publishedPort.Protocol" uib-btn-radio="'TCP'" ng-disabled="ctrl.disableLoadBalancerEdit()">TCP</label>
|
||||
<label class="btn btn-primary" ng-model="publishedPort.Protocol" uib-btn-radio="'UDP'" ng-disabled="ctrl.disableLoadBalancerEdit()">UDP</label>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removePublishedPort($index)" ng-if="!ctrl.disableLoadBalancerEdit()">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ng-repeat-end
|
||||
ng-if="
|
||||
kubernetesApplicationCreationForm['container_port_' + $index].$invalid ||
|
||||
kubernetesApplicationCreationForm['published_node_port_' + $index].$invalid ||
|
||||
kubernetesApplicationCreationForm['load_balancer_port_' + $index].$invalid
|
||||
"
|
||||
>
|
||||
<div class="col-sm-4 input-group input-group-sm">
|
||||
<div class="small text-warning" style="margin-top: 5px;" ng-if="kubernetesApplicationCreationForm['container_port_' + $index].$invalid">
|
||||
<div ng-messages="kubernetesApplicationCreationForm['container_port_'+$index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number is required.</p>
|
||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number must be inside the range 1-65535.</p>
|
||||
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number must be inside the range 1-65535.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-sm col-sm-4" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER">
|
||||
<div class="small text-warning" style="margin-top: 5px;" ng-if="kubernetesApplicationCreationForm['published_node_port_' + $index].$invalid">
|
||||
<div ng-messages="kubernetesApplicationCreationForm['published_node_port_'+$index].$error">
|
||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Node port number must be inside the range 30000-32767.</p>
|
||||
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Node port number must be inside the range 30000-32767.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4 input-group input-group-sm" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.LOAD_BALANCER">
|
||||
<div class="small text-warning" style="margin-top: 5px;" ng-if="kubernetesApplicationCreationForm['load_balancer_port_' + $index].$invalid">
|
||||
<div ng-messages="kubernetesApplicationCreationForm['load_balancer_port_'+$index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Load balancer port number is required.</p>
|
||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Load balancer port number must be inside the range 1-65535.</p>
|
||||
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Load balancer port number must be inside the range 1-65535.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group col-sm-1 input-group-sm"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !published ports -->
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled()"
|
||||
ng-click="ctrl.deployApplication()"
|
||||
button-spinner="ctrl.state.actionInProgress"
|
||||
>
|
||||
<span ng-show="!ctrl.state.isEdit && !ctrl.state.actionInProgress">Deploy application</span>
|
||||
<span ng-show="!ctrl.state.isEdit && ctrl.state.actionInProgress">Deployment in progress...</span>
|
||||
<span ng-show="ctrl.state.isEdit && !ctrl.state.actionInProgress">Update application</span>
|
||||
<span ng-show="ctrl.state.isEdit && ctrl.state.actionInProgress">Update in progress...</span>
|
||||
</button>
|
||||
<button
|
||||
ng-if="ctrl.state.isEdit && !ctrl.state.actionInProgress"
|
||||
type="button"
|
||||
class="btn btn-sm btn-default"
|
||||
ui-sref="kubernetes.applications.application({ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool })"
|
||||
>Cancel</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,8 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesCreateApplicationView', {
|
||||
templateUrl: './createApplication.html',
|
||||
controller: 'KubernetesCreateApplicationController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
},
|
||||
});
|
|
@ -0,0 +1,627 @@
|
|||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import filesizeParser from 'filesize-parser';
|
||||
import * as JsonPatch from 'fast-json-patch';
|
||||
|
||||
import {
|
||||
KubernetesApplicationDataAccessPolicies,
|
||||
KubernetesApplicationDeploymentTypes,
|
||||
KubernetesApplicationPublishingTypes,
|
||||
KubernetesApplicationQuotaDefaults,
|
||||
} from 'Kubernetes/models/application/models';
|
||||
import {
|
||||
KubernetesApplicationConfigurationFormValue,
|
||||
KubernetesApplicationConfigurationFormValueOverridenKey,
|
||||
KubernetesApplicationConfigurationFormValueOverridenKeyTypes,
|
||||
KubernetesApplicationEnvironmentVariableFormValue,
|
||||
KubernetesApplicationFormValues,
|
||||
KubernetesApplicationPersistedFolderFormValue,
|
||||
KubernetesApplicationPublishedPortFormValue,
|
||||
} from 'Kubernetes/models/application/formValues';
|
||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||
import KubernetesApplicationConverter from 'Kubernetes/converters/application';
|
||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||
|
||||
class KubernetesCreateApplicationController {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$async,
|
||||
$state,
|
||||
Notifications,
|
||||
EndpointProvider,
|
||||
Authentication,
|
||||
ModalService,
|
||||
KubernetesResourcePoolService,
|
||||
KubernetesApplicationService,
|
||||
KubernetesStackService,
|
||||
KubernetesConfigurationService,
|
||||
KubernetesNodeService,
|
||||
KubernetesPersistentVolumeClaimService
|
||||
) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.Notifications = Notifications;
|
||||
this.EndpointProvider = EndpointProvider;
|
||||
this.Authentication = Authentication;
|
||||
this.ModalService = ModalService;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
this.KubernetesStackService = KubernetesStackService;
|
||||
this.KubernetesConfigurationService = KubernetesConfigurationService;
|
||||
this.KubernetesNodeService = KubernetesNodeService;
|
||||
this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService;
|
||||
|
||||
this.ApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
||||
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
|
||||
this.ApplicationPublishingTypes = KubernetesApplicationPublishingTypes;
|
||||
this.ApplicationConfigurationFormValueOverridenKeyTypes = KubernetesApplicationConfigurationFormValueOverridenKeyTypes;
|
||||
this.ServiceTypes = KubernetesServiceTypes;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.updateApplicationAsync = this.updateApplicationAsync.bind(this);
|
||||
this.deployApplicationAsync = this.deployApplicationAsync.bind(this);
|
||||
this.updateSlidersAsync = this.updateSlidersAsync.bind(this);
|
||||
this.refreshStacksAsync = this.refreshStacksAsync.bind(this);
|
||||
this.refreshConfigurationsAsync = this.refreshConfigurationsAsync.bind(this);
|
||||
this.refreshApplicationsAsync = this.refreshApplicationsAsync.bind(this);
|
||||
this.refreshStacksConfigsAppsAsync = this.refreshStacksConfigsAppsAsync.bind(this);
|
||||
this.getApplicationAsync = this.getApplicationAsync.bind(this);
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return !this.state.alreadyExists && !this.state.hasDuplicateEnvironmentVariables && !this.state.hasDuplicatePersistedFolderPaths && !this.state.hasDuplicateConfigurationPaths;
|
||||
}
|
||||
|
||||
onChangeName() {
|
||||
const existingApplication = _.find(this.applications, { Name: this.formValues.Name });
|
||||
this.state.alreadyExists = (this.state.isEdit && existingApplication && this.application.Id !== existingApplication.Id) || (!this.state.isEdit && existingApplication);
|
||||
}
|
||||
|
||||
/**
|
||||
* CONFIGURATION UI MANAGEMENT
|
||||
*/
|
||||
addConfiguration() {
|
||||
let config = new KubernetesApplicationConfigurationFormValue();
|
||||
config.SelectedConfiguration = this.configurations[0];
|
||||
this.formValues.Configurations.push(config);
|
||||
}
|
||||
|
||||
removeConfiguration(index) {
|
||||
this.formValues.Configurations.splice(index, 1);
|
||||
this.onChangeConfigurationPath();
|
||||
}
|
||||
|
||||
overrideConfiguration(index) {
|
||||
const config = this.formValues.Configurations[index];
|
||||
config.Overriden = true;
|
||||
config.OverridenKeys = _.map(_.keys(config.SelectedConfiguration.Data), (key) => {
|
||||
const res = new KubernetesApplicationConfigurationFormValueOverridenKey();
|
||||
res.Key = key;
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
resetConfiguration(index) {
|
||||
const config = this.formValues.Configurations[index];
|
||||
config.Overriden = false;
|
||||
config.OverridenKeys = [];
|
||||
this.onChangeConfigurationPath();
|
||||
}
|
||||
|
||||
onChangeConfigurationPath() {
|
||||
this.state.duplicateConfigurationPaths = [];
|
||||
|
||||
const paths = _.reduce(
|
||||
this.formValues.Configurations,
|
||||
(result, config) => {
|
||||
const uniqOverridenKeysPath = _.uniq(_.map(config.OverridenKeys, 'Path'));
|
||||
return _.concat(result, uniqOverridenKeysPath);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const duplicatePaths = KubernetesFormValidationHelper.getDuplicates(paths);
|
||||
|
||||
_.forEach(this.formValues.Configurations, (config, index) => {
|
||||
_.forEach(config.OverridenKeys, (overridenKey, keyIndex) => {
|
||||
const findPath = _.find(duplicatePaths, (path) => path === overridenKey.Path);
|
||||
if (findPath) {
|
||||
this.state.duplicateConfigurationPaths[index + '_' + keyIndex] = findPath;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.state.hasDuplicateConfigurationPaths = Object.keys(this.state.duplicateConfigurationPaths).length > 0;
|
||||
}
|
||||
/**
|
||||
* !CONFIGURATION UI MANAGEMENT
|
||||
*/
|
||||
|
||||
/**
|
||||
* ENVIRONMENT UI MANAGEMENT
|
||||
*/
|
||||
addEnvironmentVariable() {
|
||||
this.formValues.EnvironmentVariables.push(new KubernetesApplicationEnvironmentVariableFormValue());
|
||||
}
|
||||
|
||||
hasEnvironmentVariables() {
|
||||
return this.formValues.EnvironmentVariables.length > 0;
|
||||
}
|
||||
|
||||
onChangeEnvironmentName() {
|
||||
this.state.duplicateEnvironmentVariables = KubernetesFormValidationHelper.getDuplicates(_.map(this.formValues.EnvironmentVariables, 'Name'));
|
||||
this.state.hasDuplicateEnvironmentVariables = Object.keys(this.state.duplicateEnvironmentVariables).length > 0;
|
||||
}
|
||||
|
||||
restoreEnvironmentVariable(index) {
|
||||
this.formValues.EnvironmentVariables[index].NeedsDeletion = false;
|
||||
}
|
||||
|
||||
removeEnvironmentVariable(index) {
|
||||
if (this.state.isEdit && !this.formValues.EnvironmentVariables[index].IsNew) {
|
||||
this.formValues.EnvironmentVariables[index].NeedsDeletion = true;
|
||||
} else {
|
||||
this.formValues.EnvironmentVariables.splice(index, 1);
|
||||
}
|
||||
this.onChangeEnvironmentName();
|
||||
}
|
||||
/**
|
||||
* !ENVIRONMENT UI MANAGEMENT
|
||||
*/
|
||||
|
||||
/**
|
||||
* PERSISTENT FOLDERS UI MANAGEMENT
|
||||
*/
|
||||
addPersistedFolder() {
|
||||
let storageClass = {};
|
||||
if (this.storageClasses.length > 0) {
|
||||
storageClass = this.storageClasses[0];
|
||||
}
|
||||
|
||||
this.formValues.PersistedFolders.push(new KubernetesApplicationPersistedFolderFormValue(storageClass));
|
||||
this.resetDeploymentType();
|
||||
}
|
||||
|
||||
onChangePersistedFolderPath() {
|
||||
this.state.duplicatePersistedFolderPaths = KubernetesFormValidationHelper.getDuplicates(_.map(this.formValues.PersistedFolders, 'ContainerPath'));
|
||||
this.state.hasDuplicatePersistedFolderPaths = Object.keys(this.state.duplicatePersistedFolderPaths).length > 0;
|
||||
}
|
||||
|
||||
restorePersistedFolder(index) {
|
||||
this.formValues.PersistedFolders[index].NeedsDeletion = false;
|
||||
}
|
||||
|
||||
removePersistedFolder(index) {
|
||||
if (this.state.isEdit && this.formValues.PersistedFolders[index].PersistentVolumeClaimName) {
|
||||
this.formValues.PersistedFolders[index].NeedsDeletion = true;
|
||||
} else {
|
||||
this.formValues.PersistedFolders.splice(index, 1);
|
||||
}
|
||||
this.onChangePersistedFolderPath();
|
||||
}
|
||||
/**
|
||||
* !PERSISTENT FOLDERS UI MANAGEMENT
|
||||
*/
|
||||
|
||||
/**
|
||||
* PUBLISHED PORTS UI MANAGEMENT
|
||||
*/
|
||||
addPublishedPort() {
|
||||
this.formValues.PublishedPorts.push(new KubernetesApplicationPublishedPortFormValue());
|
||||
}
|
||||
|
||||
removePublishedPort(index) {
|
||||
this.formValues.PublishedPorts.splice(index, 1);
|
||||
}
|
||||
/**
|
||||
* !PUBLISHED PORTS UI MANAGEMENT
|
||||
*/
|
||||
|
||||
/**
|
||||
* STATE VALIDATION FUNCTIONS
|
||||
*/
|
||||
storageClassAvailable() {
|
||||
return this.storageClasses && this.storageClasses.length > 0;
|
||||
}
|
||||
|
||||
hasMultipleStorageClassesAvailable() {
|
||||
return this.storageClasses && this.storageClasses.length > 1;
|
||||
}
|
||||
|
||||
resetDeploymentType() {
|
||||
this.formValues.DeploymentType = this.ApplicationDeploymentTypes.REPLICATED;
|
||||
}
|
||||
|
||||
// The data access policy panel is not shown when:
|
||||
// * There is not persisted folder specified
|
||||
showDataAccessPolicySection() {
|
||||
return this.formValues.PersistedFolders.length !== 0;
|
||||
}
|
||||
|
||||
// A global deployment is not available when either:
|
||||
// * For each persisted folder specified, if one of the storage object only supports the RWO access mode
|
||||
// * The data access policy is set to ISOLATED
|
||||
supportGlobalDeployment() {
|
||||
const hasFolders = this.formValues.PersistedFolders.length !== 0;
|
||||
const hasRWOOnly = _.find(this.formValues.PersistedFolders, (item) => _.isEqual(item.StorageClass.AccessModes, ['RWO']));
|
||||
const isIsolated = this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED;
|
||||
|
||||
if ((hasFolders && hasRWOOnly) || isIsolated) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// A StatefulSet is defined by DataAccessPolicy === ISOLATED
|
||||
isEditAndStatefulSet() {
|
||||
return this.state.isEdit && this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED;
|
||||
}
|
||||
|
||||
// A scalable deployment is available when either:
|
||||
// * No persisted folders are specified
|
||||
// * The access policy is set to shared and for each persisted folders specified, all the associated
|
||||
// storage objects support at least RWX access mode (no RWO only)
|
||||
// * The access policy is set to isolated
|
||||
supportScalableReplicaDeployment() {
|
||||
const hasFolders = this.formValues.PersistedFolders.length !== 0;
|
||||
const hasRWOOnly = _.find(this.formValues.PersistedFolders, (item) => _.isEqual(item.StorageClass.AccessModes, ['RWO']));
|
||||
const isIsolated = this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED;
|
||||
|
||||
if (!hasFolders || isIsolated || (hasFolders && !hasRWOOnly)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// For each persisted folders, returns the non scalable deployments options (storage class that only supports RWO)
|
||||
getNonScalableStorage() {
|
||||
let storageOptions = [];
|
||||
|
||||
for (let i = 0; i < this.formValues.PersistedFolders.length; i++) {
|
||||
const folder = this.formValues.PersistedFolders[i];
|
||||
|
||||
if (_.isEqual(folder.StorageClass.AccessModes, ['RWO'])) {
|
||||
storageOptions.push(folder.StorageClass.Name);
|
||||
}
|
||||
}
|
||||
|
||||
return _.uniq(storageOptions).join(', ');
|
||||
}
|
||||
|
||||
enforceReplicaCountMinimum() {
|
||||
if (this.formValues.ReplicaCount === null) {
|
||||
this.formValues.ReplicaCount = 1;
|
||||
}
|
||||
}
|
||||
|
||||
resourceQuotaCapacityExceeded() {
|
||||
return !this.state.sliders.memory.max || !this.state.sliders.cpu.max;
|
||||
}
|
||||
|
||||
resourceReservationsOverflow() {
|
||||
const instances = this.formValues.ReplicaCount;
|
||||
const cpu = this.formValues.CpuLimit;
|
||||
const maxCpu = this.state.sliders.cpu.max;
|
||||
const memory = this.formValues.MemoryLimit;
|
||||
const maxMemory = this.state.sliders.memory.max;
|
||||
|
||||
if (cpu * instances > maxCpu) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (memory * instances > maxMemory) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
publishViaLoadBalancerEnabled() {
|
||||
return this.state.useLoadBalancer;
|
||||
}
|
||||
|
||||
isEditAndNoChangesMade() {
|
||||
if (!this.state.isEdit) return false;
|
||||
const changes = JsonPatch.compare(this.savedFormValues, this.formValues);
|
||||
this.editChanges = _.filter(changes, (change) => !_.includes(change.path, '$$hashKey') && change.path !== '/ApplicationType');
|
||||
return !this.editChanges.length;
|
||||
}
|
||||
|
||||
isEditAndExistingPersistedFolder(index) {
|
||||
return this.state.isEdit && this.formValues.PersistedFolders[index].PersistentVolumeClaimName;
|
||||
}
|
||||
|
||||
isNonScalable() {
|
||||
const scalable = this.supportScalableReplicaDeployment();
|
||||
const global = this.supportGlobalDeployment();
|
||||
const replica = this.formValues.ReplicaCount > 1;
|
||||
const replicated = this.formValues.DeploymentType === this.ApplicationDeploymentTypes.REPLICATED;
|
||||
const res = (replicated && !scalable && replica) || (!replicated && !global);
|
||||
return res;
|
||||
}
|
||||
|
||||
isDeployUpdateButtonDisabled() {
|
||||
const overflow = this.resourceReservationsOverflow();
|
||||
const inProgress = this.state.actionInProgress;
|
||||
const invalid = !this.isValid();
|
||||
const hasNoChanges = this.isEditAndNoChangesMade();
|
||||
const nonScalable = this.isNonScalable();
|
||||
const res = overflow || inProgress || invalid || hasNoChanges || nonScalable;
|
||||
return res;
|
||||
}
|
||||
|
||||
disableLoadBalancerEdit() {
|
||||
return (
|
||||
this.state.isEdit &&
|
||||
this.application.ServiceType === this.ServiceTypes.LOAD_BALANCER &&
|
||||
!this.application.LoadBalancerIPAddress &&
|
||||
this.formValues.PublishingType === this.ApplicationPublishingTypes.LOAD_BALANCER
|
||||
);
|
||||
}
|
||||
/**
|
||||
* !STATE VALIDATION FUNCTIONS
|
||||
*/
|
||||
|
||||
/**
|
||||
* DATA AUTO REFRESH
|
||||
*/
|
||||
async updateSlidersAsync() {
|
||||
try {
|
||||
const quota = this.formValues.ResourcePool.Quota;
|
||||
let minCpu,
|
||||
maxCpu,
|
||||
minMemory,
|
||||
maxMemory = 0;
|
||||
if (quota) {
|
||||
this.state.resourcePoolHasQuota = true;
|
||||
if (quota.CpuLimit) {
|
||||
minCpu = KubernetesApplicationQuotaDefaults.CpuLimit;
|
||||
maxCpu = quota.CpuLimit - quota.CpuLimitUsed;
|
||||
if (this.state.isEdit && this.savedFormValues.CpuLimit) {
|
||||
maxCpu += this.savedFormValues.CpuLimit * this.savedFormValues.ReplicaCount;
|
||||
}
|
||||
} else {
|
||||
minCpu = 0;
|
||||
maxCpu = this.state.nodes.cpu;
|
||||
}
|
||||
if (quota.MemoryLimit) {
|
||||
minMemory = KubernetesApplicationQuotaDefaults.MemoryLimit;
|
||||
maxMemory = quota.MemoryLimit - quota.MemoryLimitUsed;
|
||||
if (this.state.isEdit && this.savedFormValues.MemoryLimit) {
|
||||
maxMemory += KubernetesResourceReservationHelper.bytesValue(this.savedFormValues.MemoryLimit) * this.savedFormValues.ReplicaCount;
|
||||
}
|
||||
} else {
|
||||
minMemory = 0;
|
||||
maxMemory = this.state.nodes.memory;
|
||||
}
|
||||
} else {
|
||||
this.state.resourcePoolHasQuota = false;
|
||||
minCpu = 0;
|
||||
maxCpu = this.state.nodes.cpu;
|
||||
minMemory = 0;
|
||||
maxMemory = this.state.nodes.memory;
|
||||
}
|
||||
this.state.sliders.memory.min = minMemory;
|
||||
this.state.sliders.memory.max = KubernetesResourceReservationHelper.megaBytesValue(maxMemory);
|
||||
this.state.sliders.cpu.min = minCpu;
|
||||
this.state.sliders.cpu.max = _.round(maxCpu, 2);
|
||||
if (!this.state.isEdit) {
|
||||
this.formValues.CpuLimit = minCpu;
|
||||
this.formValues.MemoryLimit = minMemory;
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to update resources selector');
|
||||
}
|
||||
}
|
||||
|
||||
updateSliders() {
|
||||
return this.$async(this.updateSlidersAsync);
|
||||
}
|
||||
|
||||
async refreshStacksAsync(namespace) {
|
||||
try {
|
||||
this.stacks = await this.KubernetesStackService.get(namespace);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve stacks');
|
||||
}
|
||||
}
|
||||
|
||||
refreshStacks(namespace) {
|
||||
return this.$async(this.refreshStacksAsync, namespace);
|
||||
}
|
||||
|
||||
async refreshConfigurationsAsync(namespace) {
|
||||
try {
|
||||
this.configurations = await this.KubernetesConfigurationService.get(namespace);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve configurations');
|
||||
}
|
||||
}
|
||||
|
||||
refreshConfigurations(namespace) {
|
||||
return this.$async(this.refreshConfigurationsAsync, namespace);
|
||||
}
|
||||
|
||||
async refreshApplicationsAsync(namespace) {
|
||||
try {
|
||||
this.applications = await this.KubernetesApplicationService.get(namespace);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve applications');
|
||||
}
|
||||
}
|
||||
|
||||
refreshApplications(namespace) {
|
||||
return this.$async(this.refreshApplicationsAsync, namespace);
|
||||
}
|
||||
|
||||
async refreshStacksConfigsAppsAsync(namespace) {
|
||||
await Promise.all([this.refreshStacks(namespace), this.refreshConfigurations(namespace), this.refreshApplications(namespace)]);
|
||||
this.onChangeName();
|
||||
}
|
||||
|
||||
refreshStacksConfigsApps(namespace) {
|
||||
return this.$async(this.refreshStacksConfigsAppsAsync, namespace);
|
||||
}
|
||||
|
||||
onResourcePoolSelectionChange() {
|
||||
const namespace = this.formValues.ResourcePool.Namespace.Name;
|
||||
this.updateSliders();
|
||||
this.refreshStacksConfigsApps(namespace);
|
||||
this.formValues.Configurations = [];
|
||||
}
|
||||
/**
|
||||
* !DATA AUTO REFRESH
|
||||
*/
|
||||
|
||||
/**
|
||||
* ACTIONS
|
||||
*/
|
||||
async deployApplicationAsync() {
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
this.formValues.ApplicationOwner = this.Authentication.getUserDetails().username;
|
||||
_.remove(this.formValues.Configurations, (item) => item.SelectedConfiguration === undefined);
|
||||
await this.KubernetesApplicationService.create(this.formValues);
|
||||
this.Notifications.success('Application successfully deployed', this.formValues.Name);
|
||||
this.$state.go('kubernetes.applications');
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to create application');
|
||||
} finally {
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
async updateApplicationAsync() {
|
||||
try {
|
||||
this.state.actionInProgress = true;
|
||||
await this.KubernetesApplicationService.patch(this.savedFormValues, this.formValues);
|
||||
this.Notifications.success('Application successfully updated');
|
||||
this.$state.go('kubernetes.applications.application', { name: this.application.Name, namespace: this.application.ResourcePool });
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve application related events');
|
||||
} finally {
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
deployApplication() {
|
||||
if (this.state.isEdit) {
|
||||
this.ModalService.confirmUpdate('Updating the application may cause a service interruption. Do you wish to continue?', (confirmed) => {
|
||||
if (confirmed) {
|
||||
return this.$async(this.updateApplicationAsync);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return this.$async(this.deployApplicationAsync);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* !ACTIONS
|
||||
*/
|
||||
|
||||
/**
|
||||
* APPLICATION - used on edit context only
|
||||
*/
|
||||
async getApplicationAsync() {
|
||||
try {
|
||||
const namespace = this.state.params.namespace;
|
||||
[this.application, this.persistentVolumeClaims] = await Promise.all([
|
||||
this.KubernetesApplicationService.get(namespace, this.state.params.name),
|
||||
this.KubernetesPersistentVolumeClaimService.get(namespace),
|
||||
]);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve application details');
|
||||
}
|
||||
}
|
||||
|
||||
getApplication() {
|
||||
return this.$async(this.getApplicationAsync);
|
||||
}
|
||||
/**
|
||||
* !APPLICATION
|
||||
*/
|
||||
|
||||
async onInit() {
|
||||
try {
|
||||
this.state = {
|
||||
actionInProgress: false,
|
||||
useLoadBalancer: false,
|
||||
sliders: {
|
||||
cpu: {
|
||||
min: 0,
|
||||
max: 0,
|
||||
},
|
||||
memory: {
|
||||
min: 0,
|
||||
max: 0,
|
||||
},
|
||||
},
|
||||
nodes: {
|
||||
memory: 0,
|
||||
cpu: 0,
|
||||
},
|
||||
resourcePoolHasQuota: false,
|
||||
viewReady: false,
|
||||
availableSizeUnits: ['MB', 'GB', 'TB'],
|
||||
alreadyExists: false,
|
||||
duplicateEnvironmentVariables: {},
|
||||
hasDuplicateEnvironmentVariables: false,
|
||||
duplicatePersistedFolderPaths: {},
|
||||
hasDuplicatePersistedFolderPaths: false,
|
||||
duplicateConfigurationPaths: {},
|
||||
hasDuplicateConfigurationPaths: false,
|
||||
isEdit: false,
|
||||
params: {
|
||||
namespace: this.$transition$.params().namespace,
|
||||
name: this.$transition$.params().name,
|
||||
},
|
||||
};
|
||||
|
||||
this.editChanges = [];
|
||||
|
||||
if (this.$transition$.params().namespace && this.$transition$.params().name) {
|
||||
this.state.isEdit = true;
|
||||
}
|
||||
|
||||
const endpoint = this.EndpointProvider.currentEndpoint();
|
||||
this.storageClasses = endpoint.Kubernetes.Configuration.StorageClasses;
|
||||
this.state.useLoadBalancer = endpoint.Kubernetes.Configuration.UseLoadBalancer;
|
||||
|
||||
this.formValues = new KubernetesApplicationFormValues();
|
||||
|
||||
const [resourcePools, nodes] = await Promise.all([this.KubernetesResourcePoolService.get(), this.KubernetesNodeService.get()]);
|
||||
|
||||
this.resourcePools = resourcePools;
|
||||
this.formValues.ResourcePool = this.resourcePools[0];
|
||||
|
||||
_.forEach(nodes, (item) => {
|
||||
this.state.nodes.memory += filesizeParser(item.Memory);
|
||||
this.state.nodes.cpu += item.CPU;
|
||||
});
|
||||
|
||||
const namespace = this.state.isEdit ? this.state.params.namespace : this.formValues.ResourcePool.Namespace.Name;
|
||||
await this.refreshStacksConfigsApps(namespace);
|
||||
|
||||
if (this.state.isEdit) {
|
||||
await this.getApplication();
|
||||
this.formValues = KubernetesApplicationConverter.applicationToFormValues(this.application, this.resourcePools, this.configurations, this.persistentVolumeClaims);
|
||||
this.savedFormValues = angular.copy(this.formValues);
|
||||
delete this.formValues.ApplicationType;
|
||||
}
|
||||
|
||||
await this.updateSliders();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||
} finally {
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesCreateApplicationController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesCreateApplicationController', KubernetesCreateApplicationController);
|
477
app/kubernetes/views/applications/edit/application.html
Normal file
477
app/kubernetes/views/applications/edit/application.html
Normal file
|
@ -0,0 +1,477 @@
|
|||
<kubernetes-view-header title="Application details" state="kubernetes.applications.application" view-ready="ctrl.state.viewReady">
|
||||
<a ui-sref="kubernetes.resourcePools">Resource pools</a> >
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.application.ResourcePool })">{{ ctrl.application.ResourcePool }}</a> >
|
||||
<a ui-sref="kubernetes.applications">Applications</a> > {{ ctrl.application.Name }}
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<uib-tabset active="ctrl.state.activeTab" justified="true" type="pills">
|
||||
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)">
|
||||
<uib-tab-heading> <i class="fa fa-laptop-code space-right" aria-hidden="true"></i> Application </uib-tab-heading>
|
||||
<div style="padding: 20px;">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
{{ ctrl.application.Name }}
|
||||
<span class="label label-primary image-tag label-margins" ng-if="!ctrl.isSystemNamespace() && ctrl.isExternalApplication()">external</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Stack</td>
|
||||
<td>{{ ctrl.application.StackName }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Resource pool</td>
|
||||
<td>
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.application.ResourcePool })">{{ ctrl.application.ResourcePool }}</a>
|
||||
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="ctrl.isSystemNamespace(item)">system</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Deployment</td>
|
||||
<td>
|
||||
<span ng-if="ctrl.application.DeploymentType === ctrl.KubernetesApplicationDeploymentTypes.REPLICATED">Replicated</span>
|
||||
<span ng-if="ctrl.application.DeploymentType === ctrl.KubernetesApplicationDeploymentTypes.GLOBAL">Global</span>
|
||||
<code>{{ ctrl.application.RunningPodsCount }}</code> / <code>{{ ctrl.application.TotalPodsCount }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="ctrl.application.Requests.Cpu || ctrl.application.Requests.Memory">
|
||||
<td>
|
||||
<div>Resource reservations</div>
|
||||
<div class="text-muted small">per instance</div>
|
||||
</td>
|
||||
<td>
|
||||
<div ng-if="ctrl.application.Requests.Cpu">CPU {{ ctrl.application.Requests.Cpu | kubernetesApplicationCPUValue }}</div>
|
||||
<div ng-if="ctrl.application.Requests.Memory">Memory {{ ctrl.application.Requests.Memory | humansize }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Creation</td>
|
||||
<td>
|
||||
<span ng-if="ctrl.application.ApplicationOwner" style="margin-right: 5px;"> <i class="fas fa-user"></i> {{ ctrl.application.ApplicationOwner }} </span>
|
||||
<span> <i class="fas fa-clock"></i> {{ ctrl.application.CreationDate | getisodate }} </span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<form class="form-horizontal" name="kubernetesApplicationNoteForm">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<i class="fa fa-edit" aria-hidden="true"></i> Note
|
||||
<button class="btn btn-xs btn-primary" ng-click="ctrl.state.expandedNote = !ctrl.state.expandedNote;"
|
||||
>{{ ctrl.state.expandedNote ? 'Collapse' : 'Expand' }}
|
||||
<i class="fas {{ ctrl.state.expandedNote ? 'fa-angle-up' : 'fa-angle-down' }}" aria-hidden="true"></i
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="ctrl.state.expandedNote">
|
||||
<div class="col-sm-12">
|
||||
<textarea
|
||||
class="form-control"
|
||||
name="application_note"
|
||||
id="application_note"
|
||||
ng-model="ctrl.formValues.Note"
|
||||
rows="5"
|
||||
placeholder="Enter a note about this application..."
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="ctrl.state.expandedNote">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
style="margin-left: 0px;"
|
||||
type="button"
|
||||
ng-click="ctrl.updateApplication()"
|
||||
ng-disabled="ctrl.formValues.Note === ctrl.application.Note"
|
||||
>{{ ctrl.application.Note ? 'Update' : 'Save' }} note</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- <tr>
|
||||
<td colspan="2">
|
||||
<form class="form-horizontal" name="KubernetesApplicationRollbackForm">
|
||||
<div class="form-group">
|
||||
<label for="resource-pool-selector" class="col-sm-2 col-lg-1 control-label text-left">Version</label>
|
||||
<div class="col-sm-2">
|
||||
<select class="form-control" id="resource-pool-selector" ng-model="ctrl.formValues.SelectedRevision"
|
||||
ng-options="revision as revision.revision for revision in ctrl.application.Revisions"></select>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<button class="btn btn-primary btn-sm" style="margin-left: 0px;" type="button" ng-click="ctrl.rollbackApplication()"
|
||||
ng-disabled="ctrl.formValues.SelectedRevision.revision === ctrl.application.CurrentRevision.revision">Rollback</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr> -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)">
|
||||
<uib-tab-heading>
|
||||
<i class="fa fa-history space-right" aria-hidden="true"></i> Events
|
||||
<div ng-if="ctrl.hasEventWarnings()">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
{{ ctrl.state.eventWarningCount }} warning(s)
|
||||
</div>
|
||||
</uib-tab-heading>
|
||||
<kubernetes-events-datatable
|
||||
title-text="Events"
|
||||
title-icon="fa-history"
|
||||
dataset="ctrl.events"
|
||||
table-key="kubernetes.application.events"
|
||||
order-by="Date"
|
||||
reverse-order="true"
|
||||
loading="ctrl.state.eventsLoading"
|
||||
refresh-callback="ctrl.getEvents"
|
||||
></kubernetes-events-datatable>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="2" ng-if="ctrl.application.Yaml" select="ctrl.showEditor()" classes="btn-sm">
|
||||
<uib-tab-heading> <i class="fa fa-code space-right" aria-hidden="true"></i> YAML </uib-tab-heading>
|
||||
<div style="padding-right: 25px;" ng-if="ctrl.state.showEditorTab">
|
||||
<kubernetes-yaml-inspector key="application-yaml" data="ctrl.application.Yaml"></kubernetes-yaml-inspector>
|
||||
</div>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div ng-if="!ctrl.isExternalApplication() && !ctrl.isSystemNamespace()" style="margin-bottom: 15px;">
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.applications.application.edit" style="margin-left: 0;">
|
||||
<i class="fa fa-file-code space-right" aria-hidden="true"></i>Edit this application
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" style="margin-left: 0;" ng-click="ctrl.redeployApplication()">
|
||||
<i class="fa fa-redo space-right" aria-hidden="true"></i>Redeploy
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" style="margin-left: 0;" ng-click="ctrl.rollbackApplication()" ng-disabled="ctrl.application.Revisions.length < 2">
|
||||
<i class="fas fa-history space-right" aria-hidden="true"></i>Rollback to previous configuration
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ACCESSING APPLICATION -->
|
||||
<div class="text-muted" style="margin-bottom: 15px;">
|
||||
<i class="fa fa-external-link-alt" aria-hidden="true" style="margin-right: 2px;"></i> Accessing the application
|
||||
</div>
|
||||
|
||||
<div class="small text-muted" ng-if="ctrl.application.PublishedPorts.length === 0" style="margin-bottom: 15px;">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This application is not exposing any port.
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.application.PublishedPorts.length > 0">
|
||||
<div ng-if="ctrl.application.ServiceType === 'LoadBalancer'">
|
||||
<div class="small text-muted">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This application is exposed through an external load balancer. Use the links below to access the different ports exposed.
|
||||
</div>
|
||||
<div style="margin-top: 10px;" class="small text-muted">
|
||||
<span ng-if="!ctrl.application.LoadBalancerIPAddress">
|
||||
<p> Load balancer status: <i class="fa fa-cog fa-spin" style="margin-left: 2px;"></i> pending </p>
|
||||
<p>
|
||||
<u>what does the "pending" status means?</u>
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="A pending status means that Portainer delegated the request to the provider responsible for creating the external load balancer. If it stays in pending state for long, this means that this capability might not be supported or you might have an issue with your cluster provider. Contact your cluster administrator for more information."
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</p>
|
||||
</span>
|
||||
<span ng-if="ctrl.application.LoadBalancerIPAddress">
|
||||
<p> Load balancer status: <i class="fa fa-check green-icon" style="margin-left: 2px;"></i> available </p>
|
||||
<p>
|
||||
Load balancer IP address: {{ ctrl.application.LoadBalancerIPAddress }}
|
||||
<span class="btn btn-primary btn-xs" ng-click="ctrl.copyLoadBalancerIP()" style="margin-left: 5px;">
|
||||
<i class="fa fa-copy space-right" aria-hidden="true"></i>Copy
|
||||
</span>
|
||||
<span id="copyNotificationLB" style="margin-left: 7px; display: none; color: #23ae89;" class="small">
|
||||
<i class="fa fa-check" aria-hidden="true"></i> copied
|
||||
</span>
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div style="margin-top: 15px; width: 33%;">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="text-muted">
|
||||
<td style="width: 50%;">Container port</td>
|
||||
<td style="width: 50%;">Load balancer port</td>
|
||||
</tr>
|
||||
<tr ng-repeat="port in ctrl.application.PublishedPorts track by $index">
|
||||
<td>{{ port.targetPort }}</td>
|
||||
<td>
|
||||
{{ port.port }}
|
||||
<a
|
||||
ng-if="ctrl.application.LoadBalancerIPAddress"
|
||||
ng-href="http://{{ ctrl.application.LoadBalancerIPAddress }}:{{ port.port }}"
|
||||
target="_blank"
|
||||
style="margin-left: 5px;"
|
||||
>
|
||||
<i class="fa fa-external-link-alt" aria-hidden="true"></i> access
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.application.ServiceType === 'NodePort'">
|
||||
<div class="small text-muted">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This application is exposed globally on all nodes of your cluster. It can be reached using the IP address of any node in your cluster using the port configuration
|
||||
below.
|
||||
</div>
|
||||
<div style="margin-top: 15px; width: 33%;">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="text-muted">
|
||||
<td style="width: 50%;">Container port</td>
|
||||
<td style="width: 50%;">Cluster node port</td>
|
||||
</tr>
|
||||
<tr ng-repeat="port in ctrl.application.PublishedPorts track by $index">
|
||||
<td>{{ port.targetPort }}</td>
|
||||
<td>{{ port.nodePort }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.application.ServiceType === 'ClusterIP'">
|
||||
<div class="small text-muted">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This application is only available for internal usage inside the cluster via the application name <code>{{ ctrl.application.ServiceName }}</code>
|
||||
<span class="btn btn-primary btn-xs" ng-click="ctrl.copyApplicationName()"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy</span>
|
||||
<span id="copyNotificationApplicationName" style="margin-left: 7px; display: none; color: #23ae89;" class="small"
|
||||
><i class="fa fa-check" aria-hidden="true"></i> copied</span
|
||||
>
|
||||
</div>
|
||||
<div class="small text-muted" style="margin-top: 2px;">
|
||||
<p>Refer to the below port configuration to access the application.</p>
|
||||
</div>
|
||||
<div style="margin-top: 15px; width: 50%;">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="text-muted">
|
||||
<td style="width: 33%;">Container port</td>
|
||||
<td style="width: 33%;">Application port</td>
|
||||
<td style="width: 33%;">Protocol</td>
|
||||
</tr>
|
||||
<tr ng-repeat="port in ctrl.application.PublishedPorts track by $index">
|
||||
<td>{{ port.targetPort }}</td>
|
||||
<td>{{ port.port }}</td>
|
||||
<td>{{ port.protocol }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !ACCESSING APPLICATION -->
|
||||
<!-- AUTO SCALING -->
|
||||
<div class="text-muted" style="margin-bottom: 15px;"> <i class="fa fa-expand-arrows-alt" aria-hidden="true" style="margin-right: 2px;"></i> Auto-scaling</div>
|
||||
|
||||
<div class="small text-muted" ng-if="!ctrl.application.AutoScaler" style="margin-bottom: 15px;">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This application does not have an autoscaling policy defined.
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.application.AutoScaler">
|
||||
<div style="margin-top: 15px; width: 50%;">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="text-muted">
|
||||
<td style="width: 33%;">Minimum instances</td>
|
||||
<td style="width: 33%;">Maximum instances</td>
|
||||
<td style="width: 33%;">
|
||||
Target CPU usage
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="The autoscaler will ensure enough instances are running to maintain an average CPU usage across all instances."
|
||||
></portainer-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ ctrl.application.AutoScaler.MinReplicas }}</td>
|
||||
<td>{{ ctrl.application.AutoScaler.MaxReplicas }}</td>
|
||||
<td>{{ ctrl.application.AutoScaler.TargetCPUUtilizationPercentage }}%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !AUTO SCALING -->
|
||||
|
||||
<!-- CONFIGURATIONS -->
|
||||
<div class="text-muted" style="margin-bottom: 15px; margin-top: 25px;"> <i class="fa fa-file-code" aria-hidden="true" style="margin-right: 2px;"></i> Configuration </div>
|
||||
|
||||
<div class="small text-muted" ng-if="!ctrl.application.Env && !ctrl.hasVolumeConfiguration()" style="margin-bottom: 15px;">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This application is not using any environment variable or configuration.
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.application.Env.length > 0">
|
||||
<div>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="text-muted">
|
||||
<td style="width: 33%;">Environment variable</td>
|
||||
<td style="width: 33%;">Value</td>
|
||||
<td style="width: 33%;">Configuration</td>
|
||||
</tr>
|
||||
<tr ng-repeat="envvar in ctrl.application.Env track by $index">
|
||||
<td>{{ envvar.name }}</td>
|
||||
<td>
|
||||
<span ng-if="envvar.value">{{ envvar.value }}</span>
|
||||
<span ng-if="envvar.valueFrom.configMapKeyRef"><i class="fa fa-key" aria-hidden="true"></i> {{ envvar.valueFrom.configMapKeyRef.key }}</span>
|
||||
<span ng-if="envvar.valueFrom.secretKeyRef"><i class="fa fa-key" aria-hidden="true"></i> {{ envvar.valueFrom.secretKeyRef.key }}</span>
|
||||
<span ng-if="envvar.valueFrom.fieldRef"
|
||||
><i class="fa fa-asterisk" aria-hidden="true"></i> {{ envvar.valueFrom.fieldRef.fieldPath }} (<a
|
||||
href="https://kubernetes.io/docs/tasks/inject-data-application/downward-api-volume-expose-pod-information/#capabilities-of-the-downward-api"
|
||||
target="_blank"
|
||||
>downward API</a
|
||||
>)</span
|
||||
>
|
||||
<span ng-if="!envvar.value && !envvar.valueFrom.secretKeyRef && !envvar.valueFrom.configMapKeyRef && !envvar.valueFrom.fieldRef">-</span>
|
||||
</td>
|
||||
<td>
|
||||
<span ng-if="envvar.value || envvar.valueFrom.fieldRef || (!envvar.valueFrom.secretKeyRef && !envvar.valueFrom.configMapKeyRef)">-</span>
|
||||
<span ng-if="envvar.valueFrom.configMapKeyRef"
|
||||
><a ui-sref="kubernetes.configurations.configuration({ name: envvar.valueFrom.configMapKeyRef.name, namespace: ctrl.application.ResourcePool })"
|
||||
><i class="fa fa-file-code" aria-hidden="true"></i> {{ envvar.valueFrom.configMapKeyRef.name }}</a
|
||||
></span
|
||||
>
|
||||
<span ng-if="envvar.valueFrom.secretKeyRef"
|
||||
><a ui-sref="kubernetes.configurations.configuration({ name: envvar.valueFrom.secretKeyRef.name, namespace: ctrl.application.ResourcePool })"
|
||||
><i class="fa fa-file-code" aria-hidden="true"></i> {{ envvar.valueFrom.secretKeyRef.name }}</a
|
||||
></span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.hasVolumeConfiguration()">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="text-muted">
|
||||
<td style="width: 33%;">Configuration path</td>
|
||||
<td style="width: 33%;">Value</td>
|
||||
<td style="width: 33%;">Configuration</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-repeat="volume in ctrl.application.ConfigurationVolumes track by $index" style="border-top: 0;">
|
||||
<td>
|
||||
{{ volume.fileMountPath }}
|
||||
</td>
|
||||
<td> <i class="fa fa-key" ng-if="volume.configurationKey" aria-hidden="true"></i> {{ volume.configurationKey ? volume.configurationKey : '-' }} </td>
|
||||
<td>
|
||||
<a ui-sref="kubernetes.configurations.configuration({ name: volume.configurationName, namespace: ctrl.application.ResourcePool })"
|
||||
><i class="fa fa-file-code" aria-hidden="true"></i> {{ volume.configurationName }}</a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- !CONFIGURATIONS -->
|
||||
|
||||
<!-- DATA PERSISTENCE -->
|
||||
<div class="text-muted" style="margin-bottom: 15px; margin-top: 25px;">
|
||||
<i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i> Data persistence
|
||||
</div>
|
||||
|
||||
<div class="small text-muted" ng-if="!ctrl.hasPersistedFolders()">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This application has no persisted folders.
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.hasPersistedFolders()">
|
||||
<div class="small text-muted" style="margin-bottom: 15px;">
|
||||
Data access policy: <i class="fa {{ ctrl.application.DataAccessPolicy | kubernetesApplicationDataAccessPolicyIcon }}" aria-hidden="true"></i>
|
||||
{{ ctrl.application.DataAccessPolicy | kubernetesApplicationDataAccessPolicyText }}
|
||||
<portainer-tooltip position="right" message="{{ ctrl.application.DataAccessPolicy | kubernetesApplicationDataAccessPolicyTooltip }}"> </portainer-tooltip>
|
||||
</div>
|
||||
|
||||
<table class="table" ng-if="ctrl.application.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED">
|
||||
<tbody>
|
||||
<tr class="text-muted">
|
||||
<td style="width: 50%;">Persisted folder</td>
|
||||
<td style="width: 50%;">Persistence</td>
|
||||
</tr>
|
||||
<tr ng-repeat="volume in ctrl.application.PersistedFolders track by $index">
|
||||
<td>
|
||||
{{ volume.MountPath }}
|
||||
</td>
|
||||
<td ng-if="volume.PersistentVolumeClaimName">
|
||||
<a ui-sref="kubernetes.volumes.volume({ name: volume.PersistentVolumeClaimName, namespace: ctrl.application.ResourcePool })"
|
||||
><i class="fa fa-database" aria-hidden="true"></i> {{ volume.PersistentVolumeClaimName }}</a
|
||||
>
|
||||
</td>
|
||||
<td ng-if="volume.HostPath"> {{ volume.HostPath }} on host filesystem </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="table" ng-if="ctrl.application.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.ISOLATED">
|
||||
<thead>
|
||||
<tr class="text-muted">
|
||||
<td style="width: 33%;">Pod</td>
|
||||
<td style="width: 33%;">Persisted folder</td>
|
||||
<td style="width: 33%;">Persistence</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody ng-repeat="pod in ctrl.application.Pods track by $index" style="border-top: none;">
|
||||
<tr ng-repeat="volume in ctrl.application.PersistedFolders track by $index">
|
||||
<td>
|
||||
{{ pod.Name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ volume.MountPath }}
|
||||
</td>
|
||||
<td ng-if="volume.PersistentVolumeClaimName">
|
||||
<a ui-sref="kubernetes.volumes.volume({ name: volume.PersistentVolumeClaimName + '-' + pod.Name, namespace: ctrl.application.ResourcePool })">
|
||||
<i class="fa fa-database" aria-hidden="true"></i> {{ volume.PersistentVolumeClaimName + '-' + pod.Name }}</a
|
||||
>
|
||||
</td>
|
||||
<td ng-if="volume.HostPath"> {{ volume.HostPath }} on host filesystem </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- !DATA PERSISTENCE -->
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<kubernetes-pods-datatable title-text="Application pods" title-icon="fa-server" dataset="ctrl.application.Pods" table-key="kubernetes.application.pods" order-by="Name">
|
||||
</kubernetes-pods-datatable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
8
app/kubernetes/views/applications/edit/application.js
Normal file
8
app/kubernetes/views/applications/edit/application.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesApplicationView', {
|
||||
templateUrl: './application.html',
|
||||
controller: 'KubernetesApplicationController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
},
|
||||
});
|
245
app/kubernetes/views/applications/edit/applicationController.js
Normal file
245
app/kubernetes/views/applications/edit/applicationController.js
Normal file
|
@ -0,0 +1,245 @@
|
|||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models';
|
||||
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
||||
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
|
||||
|
||||
class KubernetesApplicationController {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$async,
|
||||
$state,
|
||||
clipboard,
|
||||
Notifications,
|
||||
LocalStorage,
|
||||
ModalService,
|
||||
KubernetesApplicationService,
|
||||
KubernetesEventService,
|
||||
KubernetesStackService,
|
||||
KubernetesPodService,
|
||||
KubernetesNamespaceHelper
|
||||
) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.clipboard = clipboard;
|
||||
this.Notifications = Notifications;
|
||||
this.LocalStorage = LocalStorage;
|
||||
this.ModalService = ModalService;
|
||||
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
this.KubernetesEventService = KubernetesEventService;
|
||||
this.KubernetesStackService = KubernetesStackService;
|
||||
this.KubernetesPodService = KubernetesPodService;
|
||||
|
||||
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
|
||||
|
||||
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.getApplication = this.getApplication.bind(this);
|
||||
this.getApplicationAsync = this.getApplicationAsync.bind(this);
|
||||
this.getEvents = this.getEvents.bind(this);
|
||||
this.getEventsAsync = this.getEventsAsync.bind(this);
|
||||
this.updateApplicationAsync = this.updateApplicationAsync.bind(this);
|
||||
this.redeployApplicationAsync = this.redeployApplicationAsync.bind(this);
|
||||
this.rollbackApplicationAsync = this.rollbackApplicationAsync.bind(this);
|
||||
this.copyLoadBalancerIP = this.copyLoadBalancerIP.bind(this);
|
||||
}
|
||||
|
||||
selectTab(index) {
|
||||
this.LocalStorage.storeActiveTab('application', index);
|
||||
}
|
||||
|
||||
showEditor() {
|
||||
this.state.showEditorTab = true;
|
||||
this.selectTab(2);
|
||||
}
|
||||
|
||||
isSystemNamespace() {
|
||||
return this.KubernetesNamespaceHelper.isSystemNamespace(this.application.ResourcePool);
|
||||
}
|
||||
|
||||
isExternalApplication() {
|
||||
return KubernetesApplicationHelper.isExternalApplication(this.application);
|
||||
}
|
||||
|
||||
copyLoadBalancerIP() {
|
||||
this.clipboard.copyText(this.application.LoadBalancerIPAddress);
|
||||
$('#copyNotificationLB').show().fadeOut(2500);
|
||||
}
|
||||
|
||||
copyApplicationName() {
|
||||
this.clipboard.copyText(this.application.Name);
|
||||
$('#copyNotificationApplicationName').show().fadeOut(2500);
|
||||
}
|
||||
|
||||
hasPersistedFolders() {
|
||||
return this.application && this.application.PersistedFolders.length;
|
||||
}
|
||||
|
||||
hasVolumeConfiguration() {
|
||||
return this.application && this.application.ConfigurationVolumes.length;
|
||||
}
|
||||
|
||||
hasEventWarnings() {
|
||||
return this.state.eventWarningCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* ROLLBACK
|
||||
*/
|
||||
|
||||
async rollbackApplicationAsync() {
|
||||
try {
|
||||
// await this.KubernetesApplicationService.rollback(this.application, this.formValues.SelectedRevision);
|
||||
const revision = _.nth(this.application.Revisions, -2);
|
||||
await this.KubernetesApplicationService.rollback(this.application, revision);
|
||||
this.Notifications.success('Application successfully rolled back');
|
||||
this.$state.reload();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to rollback the application');
|
||||
}
|
||||
}
|
||||
|
||||
rollbackApplication() {
|
||||
this.ModalService.confirmUpdate('Rolling back the application to a previous configuration may cause a service interruption. Do you wish to continue?', (confirmed) => {
|
||||
if (confirmed) {
|
||||
return this.$async(this.rollbackApplicationAsync);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* REDEPLOY
|
||||
*/
|
||||
async redeployApplicationAsync() {
|
||||
try {
|
||||
const promises = _.map(this.application.Pods, (item) => this.KubernetesPodService.delete(item));
|
||||
await Promise.all(promises);
|
||||
this.Notifications.success('Application successfully redeployed');
|
||||
this.$state.reload();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to redeploy the application');
|
||||
}
|
||||
}
|
||||
|
||||
redeployApplication() {
|
||||
this.ModalService.confirmUpdate('Redeploying the application may cause a service interruption. Do you wish to continue?', (confirmed) => {
|
||||
if (confirmed) {
|
||||
return this.$async(this.redeployApplicationAsync);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* UPDATE
|
||||
*/
|
||||
async updateApplicationAsync() {
|
||||
try {
|
||||
const application = angular.copy(this.application);
|
||||
application.Note = this.formValues.Note;
|
||||
await this.KubernetesApplicationService.patch(this.application, application, true);
|
||||
this.Notifications.success('Application successfully updated');
|
||||
this.$state.reload();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to update application');
|
||||
}
|
||||
}
|
||||
|
||||
updateApplication() {
|
||||
return this.$async(this.updateApplicationAsync);
|
||||
}
|
||||
|
||||
/**
|
||||
* EVENTS
|
||||
*/
|
||||
async getEventsAsync() {
|
||||
try {
|
||||
this.state.eventsLoading = true;
|
||||
const events = await this.KubernetesEventService.get(this.state.params.namespace);
|
||||
this.events = _.filter(
|
||||
events,
|
||||
(event) =>
|
||||
event.Involved.uid === this.application.Id ||
|
||||
event.Involved.uid === this.application.ServiceId ||
|
||||
_.find(this.application.Pods, (pod) => pod.Id === event.Involved.uid) !== undefined
|
||||
);
|
||||
this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve application related events');
|
||||
} finally {
|
||||
this.state.eventsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getEvents() {
|
||||
return this.$async(this.getEventsAsync);
|
||||
}
|
||||
|
||||
/**
|
||||
* APPLICATION
|
||||
*/
|
||||
async getApplicationAsync() {
|
||||
try {
|
||||
this.state.dataLoading = true;
|
||||
this.application = await this.KubernetesApplicationService.get(this.state.params.namespace, this.state.params.name);
|
||||
this.formValues.Note = this.application.Note;
|
||||
if (this.application.Note) {
|
||||
this.state.expandedNote = true;
|
||||
}
|
||||
if (this.application.CurrentRevision) {
|
||||
this.formValues.SelectedRevision = _.find(this.application.Revisions, { revision: this.application.CurrentRevision.revision });
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve application details');
|
||||
} finally {
|
||||
this.state.dataLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getApplication() {
|
||||
return this.$async(this.getApplicationAsync);
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
this.state = {
|
||||
activeTab: 0,
|
||||
currentName: this.$state.$current.name,
|
||||
showEditorTab: false,
|
||||
DisplayedPanel: 'pods',
|
||||
eventsLoading: true,
|
||||
dataLoading: true,
|
||||
viewReady: false,
|
||||
params: {
|
||||
namespace: this.$transition$.params().namespace,
|
||||
name: this.$transition$.params().name,
|
||||
},
|
||||
eventWarningCount: 0,
|
||||
expandedNote: false,
|
||||
};
|
||||
|
||||
this.state.activeTab = this.LocalStorage.getActiveTab('application');
|
||||
|
||||
this.formValues = {
|
||||
Note: '',
|
||||
SelectedRevision: undefined,
|
||||
};
|
||||
|
||||
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
||||
await this.getApplication();
|
||||
await this.getEvents();
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
if (this.state.currentName !== this.$state.$current.name) {
|
||||
this.LocalStorage.storeActiveTab('application', 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesApplicationController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesApplicationController', KubernetesApplicationController);
|
62
app/kubernetes/views/applications/logs/logs.html
Normal file
62
app/kubernetes/views/applications/logs/logs.html
Normal file
|
@ -0,0 +1,62 @@
|
|||
<kubernetes-view-header title="Application logs" state="kubernetes.applications.application.logs" view-ready="ctrl.state.viewReady">
|
||||
<a ui-sref="kubernetes.resourcePools">Resource pools</a> >
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.application.ResourcePool })">{{ ctrl.application.ResourcePool }}</a> >
|
||||
<a ui-sref="kubernetes.applications">Applications</a> >
|
||||
<a ui-sref="kubernetes.applications.application({ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool })">{{ ctrl.application.Name }}</a> > Pods >
|
||||
{{ ctrl.podName }} > Logs
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady" style="height: 100%;">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" autocomplete="off">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<!-- auto-refresh -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Auto-refresh
|
||||
<portainer-tooltip position="bottom" message="Automatically refresh logs every 5 seconds"></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.state.autoRefresh" ng-change="ctrl.updateAutoRefresh()" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !auto-refresh -->
|
||||
<!-- search -->
|
||||
<div class="form-group">
|
||||
<label for="logs_search" class="col-sm-1 control-label text-left">
|
||||
Search
|
||||
</label>
|
||||
<div class="col-sm-11">
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="logs_search"
|
||||
ng-model="ctrl.state.search"
|
||||
ng-change="ctrl.state.selectedLines.length = 0;"
|
||||
placeholder="Filter..."
|
||||
auto-focus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !search -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="height: 54%;">
|
||||
<div class="col-sm-12" style="height: 100%;">
|
||||
<pre
|
||||
class="log_viewer"
|
||||
><div ng-repeat="line in ctrl.state.filteredLogs = (ctrl.applicationLogs | filter:ctrl.state.search) track by $index" class="line" ng-if="line"><p class="inner_line">{{ line }}</p></div><div ng-if="ctrl.applicationLogs.length && !ctrl.state.filteredLogs.length" class="line"><p class="inner_line">No log line matching the '{{ ctrl.state.search }}' filter</p></div><div ng-if="ctrl.applicationLogs.length === 0" class="line"><p class="inner_line">No logs available</p></div></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
8
app/kubernetes/views/applications/logs/logs.js
Normal file
8
app/kubernetes/views/applications/logs/logs.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesApplicationLogsView', {
|
||||
templateUrl: './logs.html',
|
||||
controller: 'KubernetesApplicationLogsController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
},
|
||||
});
|
87
app/kubernetes/views/applications/logs/logsController.js
Normal file
87
app/kubernetes/views/applications/logs/logsController.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
import angular from 'angular';
|
||||
|
||||
class KubernetesApplicationLogsController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, $interval, Notifications, KubernetesApplicationService, KubernetesPodService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.$interval = $interval;
|
||||
this.Notifications = Notifications;
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
this.KubernetesPodService = KubernetesPodService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.stopRepeater = this.stopRepeater.bind(this);
|
||||
this.getApplicationLogsAsync = this.getApplicationLogsAsync.bind(this);
|
||||
}
|
||||
|
||||
updateAutoRefresh() {
|
||||
if (this.state.autoRefresh) {
|
||||
this.setUpdateRepeater();
|
||||
return;
|
||||
}
|
||||
|
||||
this.stopRepeater();
|
||||
}
|
||||
|
||||
stopRepeater() {
|
||||
if (angular.isDefined(this.repeater)) {
|
||||
this.$interval.cancel(this.repeater);
|
||||
this.repeater = null;
|
||||
}
|
||||
}
|
||||
|
||||
setUpdateRepeater() {
|
||||
this.repeater = this.$interval(this.getApplicationLogsAsync, this.state.refreshRate);
|
||||
}
|
||||
|
||||
async getApplicationLogsAsync() {
|
||||
try {
|
||||
this.applicationLogs = await this.KubernetesPodService.logs(this.application.ResourcePool, this.podName);
|
||||
} catch (err) {
|
||||
this.stopRepeater();
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve application logs');
|
||||
}
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
this.state = {
|
||||
autoRefresh: false,
|
||||
refreshRate: 5000, // 5 seconds
|
||||
search: '',
|
||||
viewReady: false,
|
||||
};
|
||||
|
||||
const podName = this.$transition$.params().pod;
|
||||
const applicationName = this.$transition$.params().name;
|
||||
const namespace = this.$transition$.params().namespace;
|
||||
|
||||
this.applicationLogs = [];
|
||||
this.podName = podName;
|
||||
|
||||
try {
|
||||
const [application, applicationLogs] = await Promise.all([
|
||||
this.KubernetesApplicationService.get(namespace, applicationName),
|
||||
this.KubernetesPodService.logs(namespace, podName),
|
||||
]);
|
||||
|
||||
this.application = application;
|
||||
this.applicationLogs = applicationLogs;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve application logs');
|
||||
} finally {
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
this.stopRepeater();
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesApplicationLogsController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesApplicationLogsController', KubernetesApplicationLogsController);
|
40
app/kubernetes/views/cluster/cluster.html
Normal file
40
app/kubernetes/views/cluster/cluster.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
<kubernetes-view-header title="Cluster" state="kubernetes.cluster" view-ready="ctrl.state.viewReady">
|
||||
Cluster information
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row" ng-if="ctrl.resourceReservation && ctrl.isAdmin">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<kubernetes-resource-reservation
|
||||
cpu="ctrl.resourceReservation.CPU"
|
||||
cpu-limit="ctrl.CPULimit"
|
||||
memory="ctrl.resourceReservation.Memory"
|
||||
memory-limit="ctrl.MemoryLimit"
|
||||
description="Resource reservation represents the total amount of resource assigned to all the applications inside the cluster."
|
||||
>
|
||||
</kubernetes-resource-reservation>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<kubernetes-nodes-datatable
|
||||
title-text="Nodes"
|
||||
title-icon="fa-hdd"
|
||||
dataset="ctrl.nodes"
|
||||
table-key="kubernetes.nodes"
|
||||
order-by="Name"
|
||||
refresh-callback="ctrl.getNodes"
|
||||
is-admin="ctrl.isAdmin"
|
||||
></kubernetes-nodes-datatable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
5
app/kubernetes/views/cluster/cluster.js
Normal file
5
app/kubernetes/views/cluster/cluster.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesClusterView', {
|
||||
templateUrl: './cluster.html',
|
||||
controller: 'KubernetesClusterController',
|
||||
controllerAs: 'ctrl',
|
||||
});
|
88
app/kubernetes/views/cluster/clusterController.js
Normal file
88
app/kubernetes/views/cluster/clusterController.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import filesizeParser from 'filesize-parser';
|
||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||
import { KubernetesResourceReservation } from 'Kubernetes/models/resource-reservation/models';
|
||||
|
||||
class KubernetesClusterController {
|
||||
/* @ngInject */
|
||||
constructor($async, Authentication, Notifications, KubernetesNodeService, KubernetesApplicationService) {
|
||||
this.$async = $async;
|
||||
this.Authentication = Authentication;
|
||||
this.Notifications = Notifications;
|
||||
this.KubernetesNodeService = KubernetesNodeService;
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.getNodes = this.getNodes.bind(this);
|
||||
this.getNodesAsync = this.getNodesAsync.bind(this);
|
||||
this.getApplicationsAsync = this.getApplicationsAsync.bind(this);
|
||||
}
|
||||
|
||||
async getNodesAsync() {
|
||||
try {
|
||||
const nodes = await this.KubernetesNodeService.get();
|
||||
_.forEach(nodes, (node) => (node.Memory = filesizeParser(node.Memory)));
|
||||
this.nodes = nodes;
|
||||
this.CPULimit = _.reduce(this.nodes, (acc, node) => node.CPU + acc, 0);
|
||||
this.MemoryLimit = _.reduce(this.nodes, (acc, node) => KubernetesResourceReservationHelper.megaBytesValue(node.Memory) + acc, 0);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve nodes');
|
||||
}
|
||||
}
|
||||
|
||||
getNodes() {
|
||||
return this.$async(this.getNodesAsync);
|
||||
}
|
||||
|
||||
async getApplicationsAsync() {
|
||||
try {
|
||||
this.state.applicationsLoading = true;
|
||||
this.applications = await this.KubernetesApplicationService.get();
|
||||
const nodeNames = _.map(this.nodes, (node) => node.Name);
|
||||
this.resourceReservation = _.reduce(
|
||||
this.applications,
|
||||
(acc, app) => {
|
||||
app.Pods = _.filter(app.Pods, (pod) => nodeNames.includes(pod.Node));
|
||||
const resourceReservation = KubernetesResourceReservationHelper.computeResourceReservation(app.Pods);
|
||||
acc.CPU += resourceReservation.CPU;
|
||||
acc.Memory += resourceReservation.Memory;
|
||||
return acc;
|
||||
},
|
||||
new KubernetesResourceReservation()
|
||||
);
|
||||
this.resourceReservation.Memory = KubernetesResourceReservationHelper.megaBytesValue(this.resourceReservation.Memory);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', 'Unable to retrieve applications', err);
|
||||
} finally {
|
||||
this.state.applicationsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getApplications() {
|
||||
return this.$async(this.getApplicationsAsync);
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
this.state = {
|
||||
applicationsLoading: true,
|
||||
viewReady: false,
|
||||
};
|
||||
|
||||
this.isAdmin = this.Authentication.isAdmin();
|
||||
|
||||
await this.getNodes();
|
||||
if (this.isAdmin) {
|
||||
await this.getApplications();
|
||||
}
|
||||
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesClusterController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesClusterController', KubernetesClusterController);
|
107
app/kubernetes/views/cluster/node/node.html
Normal file
107
app/kubernetes/views/cluster/node/node.html
Normal file
|
@ -0,0 +1,107 @@
|
|||
<kubernetes-view-header title="Node details" state="kubernetes.cluster.node" view-ready="ctrl.state.viewReady">
|
||||
<a ui-sref="kubernetes.cluster">Cluster</a> > {{ ctrl.node.Name }}
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<uib-tabset active="ctrl.state.activeTab" justified="true" type="pills">
|
||||
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)">
|
||||
<uib-tab-heading> <i class="fa fa-hdd space-right" aria-hidden="true"></i> Node </uib-tab-heading>
|
||||
|
||||
<form class="form-horizontal" style="padding: 20px;">
|
||||
<table class="table">
|
||||
<tbody ng-if="ctrl.node">
|
||||
<tr>
|
||||
<td>Hostname</td>
|
||||
<td>{{ ctrl.node.Name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Role</td>
|
||||
<td>{{ ctrl.node.Role }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kubelet version</td>
|
||||
<td>{{ ctrl.node.Version }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Creation date</td>
|
||||
<td>{{ ctrl.node.CreationDate | getisodate }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>
|
||||
<span class="label label-{{ ctrl.node.Status | kubernetesNodeStatusColor }}">
|
||||
{{ ctrl.node.Status }}
|
||||
</span>
|
||||
<span ng-if="ctrl.node.Status == 'Warning'" class="text text-warning">
|
||||
{{ ctrl.node.Conditions | kubernetesNodeConditionsMessage }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="padding: 8px;">
|
||||
<kubernetes-resource-reservation
|
||||
ng-if="ctrl.resourceReservation"
|
||||
cpu="ctrl.resourceReservation.CPU"
|
||||
cpu-limit="ctrl.node.CPU"
|
||||
memory="ctrl.resourceReservation.Memory"
|
||||
memory-limit="ctrl.memoryLimit"
|
||||
description="Resource reservation represents the total amount of resource assigned to all the applications running on this node."
|
||||
>
|
||||
</kubernetes-resource-reservation>
|
||||
</div>
|
||||
</form>
|
||||
</uib-tab>
|
||||
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)">
|
||||
<uib-tab-heading>
|
||||
<i class="fa fa-history space-right" aria-hidden="true"></i> Events
|
||||
<div ng-if="ctrl.hasEventWarnings()">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
{{ ctrl.state.eventWarningCount }} warning(s)
|
||||
</div>
|
||||
</uib-tab-heading>
|
||||
<kubernetes-events-datatable
|
||||
title-text="Events"
|
||||
title-icon="fa-history"
|
||||
dataset="ctrl.events"
|
||||
table-key="kubernetes.node.events"
|
||||
order-by="Date"
|
||||
reverse-order="true"
|
||||
loading="ctrl.state.eventsLoading"
|
||||
refresh-callback="ctrl.getEvents"
|
||||
>
|
||||
</kubernetes-events-datatable>
|
||||
</uib-tab>
|
||||
<uib-tab index="2" ng-if="ctrl.node.Yaml" select="ctrl.showEditor()" classes="btn-sm">
|
||||
<uib-tab-heading> <i class="fa fa-code space-right" aria-hidden="true"></i> YAML </uib-tab-heading>
|
||||
<div style="padding-right: 25px;" ng-if="ctrl.state.showEditorTab">
|
||||
<kubernetes-yaml-inspector key="node-yaml" data="ctrl.node.Yaml"> </kubernetes-yaml-inspector>
|
||||
</div>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="ctrl.applications && ctrl.applications.length > 0">
|
||||
<div class="col-sm-12">
|
||||
<kubernetes-node-applications-datatable
|
||||
dataset="ctrl.applications"
|
||||
table-key="kubernetes.node.applications"
|
||||
order-by="Name"
|
||||
refresh-callback="ctrl.getApplications"
|
||||
loading="ctrl.state.applicationsLoading"
|
||||
title-text="Applications running on this node"
|
||||
title-icon="fa-laptop-code"
|
||||
>
|
||||
</kubernetes-node-applications-datatable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
8
app/kubernetes/views/cluster/node/node.js
Normal file
8
app/kubernetes/views/cluster/node/node.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesNodeView', {
|
||||
templateUrl: './node.html',
|
||||
controller: 'KubernetesNodeController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
},
|
||||
});
|
137
app/kubernetes/views/cluster/node/nodeController.js
Normal file
137
app/kubernetes/views/cluster/node/nodeController.js
Normal file
|
@ -0,0 +1,137 @@
|
|||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||
import { KubernetesResourceReservation } from 'Kubernetes/models/resource-reservation/models';
|
||||
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
||||
|
||||
class KubernetesNodeController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, Notifications, LocalStorage, KubernetesNodeService, KubernetesEventService, KubernetesPodService, KubernetesApplicationService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.Notifications = Notifications;
|
||||
this.LocalStorage = LocalStorage;
|
||||
this.KubernetesNodeService = KubernetesNodeService;
|
||||
this.KubernetesEventService = KubernetesEventService;
|
||||
this.KubernetesPodService = KubernetesPodService;
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.getNodeAsync = this.getNodeAsync.bind(this);
|
||||
this.getEvents = this.getEvents.bind(this);
|
||||
this.getEventsAsync = this.getEventsAsync.bind(this);
|
||||
this.getApplicationsAsync = this.getApplicationsAsync.bind(this);
|
||||
}
|
||||
|
||||
selectTab(index) {
|
||||
this.LocalStorage.storeActiveTab('node', index);
|
||||
}
|
||||
|
||||
async getNodeAsync() {
|
||||
try {
|
||||
this.state.dataLoading = true;
|
||||
const nodeName = this.$transition$.params().name;
|
||||
this.node = await this.KubernetesNodeService.get(nodeName);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve node');
|
||||
} finally {
|
||||
this.state.dataLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getNode() {
|
||||
return this.$async(this.getNodeAsync);
|
||||
}
|
||||
|
||||
hasEventWarnings() {
|
||||
return this.state.eventWarningCount;
|
||||
}
|
||||
|
||||
async getEventsAsync() {
|
||||
try {
|
||||
this.state.eventsLoading = true;
|
||||
this.events = await this.KubernetesEventService.get();
|
||||
this.events = _.filter(this.events.items, (item) => item.involvedObject.kind === 'Node');
|
||||
this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve node events');
|
||||
} finally {
|
||||
this.state.eventsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getEvents() {
|
||||
return this.$async(this.getEventsAsync);
|
||||
}
|
||||
|
||||
showEditor() {
|
||||
this.state.showEditorTab = true;
|
||||
this.selectTab(2);
|
||||
}
|
||||
|
||||
async getApplicationsAsync() {
|
||||
try {
|
||||
this.state.applicationsLoading = true;
|
||||
this.applications = await this.KubernetesApplicationService.get();
|
||||
|
||||
this.resourceReservation = new KubernetesResourceReservation();
|
||||
this.applications = _.map(this.applications, (app) => {
|
||||
app.Pods = _.filter(app.Pods, (pod) => pod.Node === this.node.Name);
|
||||
return app;
|
||||
});
|
||||
this.applications = _.filter(this.applications, (app) => app.Pods.length !== 0);
|
||||
this.applications = _.map(this.applications, (app) => {
|
||||
const resourceReservation = KubernetesResourceReservationHelper.computeResourceReservation(app.Pods);
|
||||
app.CPU = resourceReservation.CPU;
|
||||
app.Memory = resourceReservation.Memory;
|
||||
this.resourceReservation.CPU += resourceReservation.CPU;
|
||||
this.resourceReservation.Memory += resourceReservation.Memory;
|
||||
return app;
|
||||
});
|
||||
this.resourceReservation.Memory = KubernetesResourceReservationHelper.megaBytesValue(this.resourceReservation.Memory);
|
||||
this.memoryLimit = KubernetesResourceReservationHelper.megaBytesValue(this.node.Memory);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve applications');
|
||||
} finally {
|
||||
this.state.applicationsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getApplications() {
|
||||
return this.$async(this.getApplicationsAsync);
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
this.state = {
|
||||
activeTab: 0,
|
||||
currentName: this.$state.$current.name,
|
||||
dataLoading: true,
|
||||
eventsLoading: true,
|
||||
applicationsLoading: true,
|
||||
showEditorTab: false,
|
||||
viewReady: false,
|
||||
eventWarningCount: 0,
|
||||
};
|
||||
|
||||
this.state.activeTab = this.LocalStorage.getActiveTab('node');
|
||||
|
||||
await this.getNode();
|
||||
await this.getEvents();
|
||||
await this.getApplications();
|
||||
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
if (this.state.currentName !== this.$state.$current.name) {
|
||||
this.LocalStorage.storeActiveTab('node', 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesNodeController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesNodeController', KubernetesNodeController);
|
19
app/kubernetes/views/configurations/configurations.html
Normal file
19
app/kubernetes/views/configurations/configurations.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
<kubernetes-view-header title="Configuration list" state="kubernetes.configurations" view-ready="ctrl.state.viewReady">
|
||||
Configurations
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<kubernetes-configurations-datatable
|
||||
dataset="ctrl.configurations"
|
||||
table-key="kubernetes.configurations"
|
||||
order-by="Name"
|
||||
refresh-callback="ctrl.refreshCallback"
|
||||
remove-action="ctrl.removeAction"
|
||||
></kubernetes-configurations-datatable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
5
app/kubernetes/views/configurations/configurations.js
Normal file
5
app/kubernetes/views/configurations/configurations.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesConfigurationsView', {
|
||||
templateUrl: './configurations.html',
|
||||
controller: 'KubernetesConfigurationsController',
|
||||
controllerAs: 'ctrl',
|
||||
});
|
105
app/kubernetes/views/configurations/configurationsController.js
Normal file
105
app/kubernetes/views/configurations/configurationsController.js
Normal file
|
@ -0,0 +1,105 @@
|
|||
import angular from 'angular';
|
||||
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
|
||||
|
||||
class KubernetesConfigurationsController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, Notifications, KubernetesConfigurationService, KubernetesApplicationService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.Notifications = Notifications;
|
||||
this.KubernetesConfigurationService = KubernetesConfigurationService;
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.getConfigurations = this.getConfigurations.bind(this);
|
||||
this.getConfigurationsAsync = this.getConfigurationsAsync.bind(this);
|
||||
this.getApplicationsAsync = this.getApplicationsAsync.bind(this);
|
||||
this.removeAction = this.removeAction.bind(this);
|
||||
this.removeActionAsync = this.removeActionAsync.bind(this);
|
||||
this.refreshCallback = this.refreshCallback.bind(this);
|
||||
this.refreshCallbackAsync = this.refreshCallbackAsync.bind(this);
|
||||
}
|
||||
|
||||
async getConfigurationsAsync() {
|
||||
try {
|
||||
this.state.configurationsLoading = true;
|
||||
this.configurations = await this.KubernetesConfigurationService.get();
|
||||
KubernetesConfigurationHelper.setConfigurationsUsed(this.configurations, this.applications);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve configurations');
|
||||
} finally {
|
||||
this.state.configurationsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getConfigurations() {
|
||||
return this.$async(this.getConfigurationsAsync);
|
||||
}
|
||||
|
||||
async removeActionAsync(selectedItems) {
|
||||
let actionCount = selectedItems.length;
|
||||
for (const configuration of selectedItems) {
|
||||
try {
|
||||
await this.KubernetesConfigurationService.delete(configuration);
|
||||
this.Notifications.success('Configurations successfully removed', configuration.Name);
|
||||
const index = this.configurations.indexOf(configuration);
|
||||
this.configurations.splice(index, 1);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to remove configuration');
|
||||
} finally {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
this.$state.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeAction(selectedItems) {
|
||||
return this.$async(this.removeActionAsync, selectedItems);
|
||||
}
|
||||
|
||||
async getApplicationsAsync() {
|
||||
try {
|
||||
this.state.applicationsLoading = true;
|
||||
this.applications = await this.KubernetesApplicationService.get();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve applications');
|
||||
} finally {
|
||||
this.state.applicationsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getApplications() {
|
||||
return this.$async(this.getApplicationsAsync);
|
||||
}
|
||||
|
||||
async refreshCallbackAsync() {
|
||||
await this.getConfigurations();
|
||||
await this.getApplications();
|
||||
}
|
||||
|
||||
refreshCallback() {
|
||||
return this.$async(this.refreshCallbackAsync);
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
this.state = {
|
||||
configurationsLoading: true,
|
||||
applicationsLoading: true,
|
||||
viewReady: false,
|
||||
};
|
||||
|
||||
await this.getApplications();
|
||||
await this.getConfigurations();
|
||||
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesConfigurationsController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesConfigurationsController', KubernetesConfigurationsController);
|
|
@ -0,0 +1,135 @@
|
|||
<kubernetes-view-header title="Create configuration" state="kubernetes.configurations.new" view-ready="ctrl.state.viewReady">
|
||||
<a ui-sref="kubernetes.configurations">Configurations</a> > Create a configuration
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
|
||||
<!-- name -->
|
||||
<div class="form-group">
|
||||
<label for="configuration_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="configuration_name"
|
||||
ng-model="ctrl.formValues.Name"
|
||||
ng-pattern="/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/"
|
||||
ng-change="ctrl.onChangeName()"
|
||||
placeholder="my-configuration"
|
||||
auto-focus
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="kubernetesConfigurationCreationForm.configuration_name.$invalid || ctrl.state.alreadyExist">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="kubernetesConfigurationCreationForm.configuration_name.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
<p ng-message="pattern"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist of lower case alphanumeric characters, '-' or '.', and must start and end
|
||||
with an alphanumeric character.</p
|
||||
>
|
||||
</div>
|
||||
<p ng-if="ctrl.state.alreadyExist"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A configuration with the same name already exists inside the selected resource pool.</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name -->
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Resource pool
|
||||
</div>
|
||||
|
||||
<!-- resource-pool -->
|
||||
<div class="form-group">
|
||||
<label for="resource-pool-selector" class="col-sm-1 control-label text-left">Resource pool</label>
|
||||
<div class="col-sm-11">
|
||||
<select
|
||||
class="form-control"
|
||||
id="resource-pool-selector"
|
||||
ng-model="ctrl.formValues.ResourcePool"
|
||||
ng-options="resourcePool.Namespace.Name for resourcePool in ctrl.resourcePools"
|
||||
ng-change="ctrl.onResourcePoolSelectionChange()"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded()">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This resource pool has exhausted its resource capacity and you will not be able to deploy the configuration. Contact your administrator to expand the capacity of
|
||||
the resource pool.
|
||||
</div>
|
||||
</div>
|
||||
<!-- !resource-pool -->
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Configuration type
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
Select the type of data that you want to save in the configuration.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- type options -->
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<div class="boxselector_wrapper">
|
||||
<div>
|
||||
<input type="radio" id="type_basic" ng-value="ctrl.KubernetesConfigurationTypes.CONFIGMAP" ng-model="ctrl.formValues.Type" />
|
||||
<label for="type_basic">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-file-code" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Non-sensitive
|
||||
</div>
|
||||
<p>This configuration holds non-sensitive information</p>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="type_secret" ng-value="ctrl.KubernetesConfigurationTypes.SECRET" ng-model="ctrl.formValues.Type" />
|
||||
<label for="type_secret">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-user-secret" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Sensitive
|
||||
</div>
|
||||
<p>This configuration holds sensitive information</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !type options -->
|
||||
|
||||
<kubernetes-configuration-data ng-if="ctrl.formValues" form-values="ctrl.formValues" is-valid="ctrl.state.isDataValid"></kubernetes-configuration-data>
|
||||
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title" style="margin-top: 10px;">
|
||||
Actions
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="!kubernetesConfigurationCreationForm.$valid || !ctrl.isFormValid() || ctrl.state.actionInProgress"
|
||||
ng-click="ctrl.createConfiguration()"
|
||||
button-spinner="ctrl.state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="ctrl.state.actionInProgress">Create configuration</span>
|
||||
<span ng-show="ctrl.state.actionInProgress">Creation in progress...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,5 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesCreateConfigurationView', {
|
||||
templateUrl: './createConfiguration.html',
|
||||
controller: 'KubernetesCreateConfigurationController',
|
||||
controllerAs: 'ctrl',
|
||||
});
|
|
@ -0,0 +1,93 @@
|
|||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import { KubernetesConfigurationFormValues, KubernetesConfigurationFormValuesDataEntry } from 'Kubernetes/models/configuration/formvalues';
|
||||
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
|
||||
|
||||
class KubernetesCreateConfigurationController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, Notifications, Authentication, KubernetesConfigurationService, KubernetesResourcePoolService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.Notifications = Notifications;
|
||||
this.Authentication = Authentication;
|
||||
this.KubernetesConfigurationService = KubernetesConfigurationService;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
this.KubernetesConfigurationTypes = KubernetesConfigurationTypes;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.createConfigurationAsync = this.createConfigurationAsync.bind(this);
|
||||
this.getConfigurationsAsync = this.getConfigurationsAsync.bind(this);
|
||||
}
|
||||
|
||||
onChangeName() {
|
||||
const filteredConfigurations = _.filter(this.configurations, (config) => config.Namespace === this.formValues.ResourcePool.Namespace.Name);
|
||||
this.state.alreadyExist = _.find(filteredConfigurations, (config) => config.Name === this.formValues.Name) !== undefined;
|
||||
}
|
||||
|
||||
isFormValid() {
|
||||
const uniqueCheck = !this.state.alreadyExist && this.state.isDataValid;
|
||||
if (this.formValues.IsSimple) {
|
||||
return this.formValues.Data.length > 0 && uniqueCheck;
|
||||
}
|
||||
return uniqueCheck;
|
||||
}
|
||||
|
||||
async createConfigurationAsync() {
|
||||
try {
|
||||
this.state.actionInProgress = true;
|
||||
this.formValues.ConfigurationOwner = this.Authentication.getUserDetails().username;
|
||||
await this.KubernetesConfigurationService.create(this.formValues);
|
||||
this.Notifications.success('Configuration succesfully created');
|
||||
this.$state.go('kubernetes.configurations');
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to create configuration');
|
||||
} finally {
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
createConfiguration() {
|
||||
return this.$async(this.createConfigurationAsync);
|
||||
}
|
||||
|
||||
async getConfigurationsAsync() {
|
||||
try {
|
||||
this.configurations = await this.KubernetesConfigurationService.get();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve configurations');
|
||||
}
|
||||
}
|
||||
|
||||
getConfigurations() {
|
||||
return this.$async(this.getConfigurationsAsync);
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
this.state = {
|
||||
actionInProgress: false,
|
||||
viewReady: false,
|
||||
alreadyExist: false,
|
||||
isDataValid: true,
|
||||
};
|
||||
|
||||
this.formValues = new KubernetesConfigurationFormValues();
|
||||
this.formValues.Data.push(new KubernetesConfigurationFormValuesDataEntry());
|
||||
|
||||
try {
|
||||
this.resourcePools = await this.KubernetesResourcePoolService.get();
|
||||
this.formValues.ResourcePool = this.resourcePools[0];
|
||||
await this.getConfigurations();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||
} finally {
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesCreateConfigurationController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesCreateConfigurationController', KubernetesCreateConfigurationController);
|
120
app/kubernetes/views/configurations/edit/configuration.html
Normal file
120
app/kubernetes/views/configurations/edit/configuration.html
Normal file
|
@ -0,0 +1,120 @@
|
|||
<kubernetes-view-header title="Configuration details" state="kubernetes.configurations.configuration" view-ready="ctrl.state.viewReady">
|
||||
<a ui-sref="kubernetes.resourcePools">Resource pools</a> >
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.configuration.Namespace })">{{ ctrl.configuration.Namespace }}</a> >
|
||||
<a ui-sref="kubernetes.configurations">Configurations</a> > {{ ctrl.configuration.Name }}
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<uib-tabset active="ctrl.state.activeTab" justified="true" type="pills">
|
||||
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)">
|
||||
<uib-tab-heading> <i class="fa fa-file-code space-right" aria-hidden="true"></i> Configuration </uib-tab-heading>
|
||||
<div style="padding: 20px;">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
{{ ctrl.configuration.Name }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Resource Pool</td>
|
||||
<td>
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.configuration.Namespace })">{{ ctrl.configuration.Namespace }}</a>
|
||||
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="ctrl.isSystemNamespace()">system</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Configuration type</td>
|
||||
<td>
|
||||
{{ ctrl.configuration.Type | kubernetesConfigurationTypeText }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</uib-tab>
|
||||
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)">
|
||||
<uib-tab-heading>
|
||||
<i class="fa fa-history space-right" aria-hidden="true"></i> Events
|
||||
<div ng-if="ctrl.hasEventWarnings()">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
{{ ctrl.state.eventWarningCount }} warning(s)
|
||||
</div>
|
||||
</uib-tab-heading>
|
||||
<kubernetes-events-datatable
|
||||
title-text="Events"
|
||||
title-icon="fa-history"
|
||||
dataset="ctrl.events"
|
||||
table-key="kubernetes.configuration.events"
|
||||
order-by="Date"
|
||||
reverse-order="true"
|
||||
loading="ctrl.state.eventsLoading"
|
||||
refresh-callback="ctrl.getEvents"
|
||||
>
|
||||
</kubernetes-events-datatable>
|
||||
</uib-tab>
|
||||
<uib-tab index="2" ng-if="ctrl.configuration.Yaml" classes="btn-sm" select="ctrl.showEditor()">
|
||||
<uib-tab-heading> <i class="fa fa-code space-right" aria-hidden="true"></i> YAML </uib-tab-heading>
|
||||
<div style="padding-right: 25px;" ng-if="ctrl.state.showEditorTab">
|
||||
<kubernetes-yaml-inspector key="configuration-yaml" data="ctrl.configuration.Yaml"> </kubernetes-yaml-inspector>
|
||||
</div>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
|
||||
<kubernetes-configuration-data ng-if="ctrl.formValues" form-values="ctrl.formValues" is-valid="ctrl.state.isDataValid"></kubernetes-configuration-data>
|
||||
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title" style="margin-top: 10px;">
|
||||
Actions
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="!ctrl.isFormValid() || !kubernetesConfigurationCreationForm.$valid || ctrl.state.actionInProgress"
|
||||
ng-click="ctrl.updateConfiguration()"
|
||||
button-spinner="ctrl.state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="ctrl.state.actionInProgress">Update configuration</span>
|
||||
<span ng-show="ctrl.state.actionInProgress">Update in progress...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="ctrl.configuration.Used">
|
||||
<div class="col-sm-12">
|
||||
<kubernetes-integrated-applications-datatable
|
||||
dataset="ctrl.configuration.Applications"
|
||||
table-key="kubernetes.configurations.applications"
|
||||
order-by="Name"
|
||||
refresh-callback="ctrl.getApplications"
|
||||
title-text="Applications using this configuration"
|
||||
title-icon="fa-laptop-code"
|
||||
>
|
||||
</kubernetes-integrated-applications-datatable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,8 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesConfigurationView', {
|
||||
templateUrl: './configuration.html',
|
||||
controller: 'KubernetesConfigurationController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
},
|
||||
});
|
|
@ -0,0 +1,236 @@
|
|||
import angular from 'angular';
|
||||
import { KubernetesConfigurationFormValues, KubernetesConfigurationFormValuesDataEntry } from 'Kubernetes/models/configuration/formvalues';
|
||||
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
|
||||
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
|
||||
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
||||
import _ from 'lodash-es';
|
||||
|
||||
class KubernetesConfigurationController {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$async,
|
||||
$state,
|
||||
Notifications,
|
||||
LocalStorage,
|
||||
KubernetesConfigurationService,
|
||||
KubernetesResourcePoolService,
|
||||
ModalService,
|
||||
KubernetesApplicationService,
|
||||
KubernetesEventService,
|
||||
KubernetesNamespaceHelper
|
||||
) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.Notifications = Notifications;
|
||||
this.LocalStorage = LocalStorage;
|
||||
this.ModalService = ModalService;
|
||||
this.KubernetesConfigurationService = KubernetesConfigurationService;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
this.KubernetesEventService = KubernetesEventService;
|
||||
this.KubernetesConfigurationTypes = KubernetesConfigurationTypes;
|
||||
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.getConfigurationAsync = this.getConfigurationAsync.bind(this);
|
||||
this.getEvents = this.getEvents.bind(this);
|
||||
this.getEventsAsync = this.getEventsAsync.bind(this);
|
||||
this.getApplications = this.getApplications.bind(this);
|
||||
this.getApplicationsAsync = this.getApplicationsAsync.bind(this);
|
||||
this.getConfigurationsAsync = this.getConfigurationsAsync.bind(this);
|
||||
this.updateConfiguration = this.updateConfiguration.bind(this);
|
||||
this.updateConfigurationAsync = this.updateConfigurationAsync.bind(this);
|
||||
}
|
||||
|
||||
isSystemNamespace() {
|
||||
return this.KubernetesNamespaceHelper.isSystemNamespace(this.configuration.Namespace);
|
||||
}
|
||||
|
||||
selectTab(index) {
|
||||
this.LocalStorage.storeActiveTab('configuration', index);
|
||||
}
|
||||
|
||||
showEditor() {
|
||||
this.state.showEditorTab = true;
|
||||
this.selectTab(2);
|
||||
}
|
||||
|
||||
isFormValid() {
|
||||
if (this.formValues.IsSimple) {
|
||||
return this.formValues.Data.length > 0 && this.state.isDataValid;
|
||||
}
|
||||
return this.state.isDataValid;
|
||||
}
|
||||
|
||||
async updateConfigurationAsync() {
|
||||
try {
|
||||
this.state.actionInProgress = true;
|
||||
if (
|
||||
this.formValues.Type !== this.configuration.Type ||
|
||||
this.formValues.ResourcePool.Namespace.Name !== this.configuration.Namespace ||
|
||||
this.formValues.Name !== this.configuration.Name
|
||||
) {
|
||||
await this.KubernetesConfigurationService.create(this.formValues);
|
||||
await this.KubernetesConfigurationService.delete(this.configuration);
|
||||
this.Notifications.success('Configuration succesfully updated');
|
||||
this.$state.go(
|
||||
'kubernetes.configurations.configuration',
|
||||
{
|
||||
namespace: this.formValues.ResourcePool.Namespace.Name,
|
||||
name: this.formValues.Name,
|
||||
},
|
||||
{ reload: true }
|
||||
);
|
||||
} else {
|
||||
await this.KubernetesConfigurationService.update(this.formValues);
|
||||
this.Notifications.success('Configuration succesfully updated');
|
||||
this.$state.reload();
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to update configuration');
|
||||
} finally {
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateConfiguration() {
|
||||
if (this.configuration.Used) {
|
||||
const plural = this.configuration.Applications.length > 1 ? 's' : '';
|
||||
this.ModalService.confirmUpdate(
|
||||
`The changes will be propagated to ${this.configuration.Applications.length} running application${plural}. Are you sure you want to update this configuration?`,
|
||||
(confirmed) => {
|
||||
if (confirmed) {
|
||||
return this.$async(this.updateConfigurationAsync);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return this.$async(this.updateConfigurationAsync);
|
||||
}
|
||||
}
|
||||
|
||||
async getConfigurationAsync() {
|
||||
try {
|
||||
this.state.configurationLoading = true;
|
||||
const name = this.$transition$.params().name;
|
||||
const namespace = this.$transition$.params().namespace;
|
||||
this.configuration = await this.KubernetesConfigurationService.get(namespace, name);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve configuration');
|
||||
} finally {
|
||||
this.state.configurationLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getConfiguration() {
|
||||
return this.$async(this.getConfigurationAsync);
|
||||
}
|
||||
|
||||
async getApplicationsAsync(namespace) {
|
||||
try {
|
||||
this.state.applicationsLoading = true;
|
||||
const applications = await this.KubernetesApplicationService.get(namespace);
|
||||
this.configuration.Applications = KubernetesConfigurationHelper.getUsingApplications(this.configuration, applications);
|
||||
KubernetesConfigurationHelper.setConfigurationUsed(this.configuration);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve applications');
|
||||
} finally {
|
||||
this.state.applicationsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getApplications(namespace) {
|
||||
return this.$async(this.getApplicationsAsync, namespace);
|
||||
}
|
||||
|
||||
hasEventWarnings() {
|
||||
return this.state.eventWarningCount;
|
||||
}
|
||||
|
||||
async getEventsAsync(namespace) {
|
||||
try {
|
||||
this.state.eventsLoading = true;
|
||||
this.events = await this.KubernetesEventService.get(namespace);
|
||||
this.events = _.filter(this.events, (event) => event.Involved.uid === this.configuration.Id);
|
||||
this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events);
|
||||
} catch (err) {
|
||||
this.Notifications('Failure', err, 'Unable to retrieve events');
|
||||
} finally {
|
||||
this.state.eventsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getEvents(namespace) {
|
||||
return this.$async(this.getEventsAsync, namespace);
|
||||
}
|
||||
|
||||
async getConfigurationsAsync() {
|
||||
try {
|
||||
this.configurations = await this.KubernetesConfigurationService.get();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve configurations');
|
||||
}
|
||||
}
|
||||
|
||||
getConfigurations() {
|
||||
return this.$async(this.getConfigurationsAsync);
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
try {
|
||||
this.state = {
|
||||
actionInProgress: false,
|
||||
configurationLoading: true,
|
||||
applicationsLoading: true,
|
||||
eventsLoading: true,
|
||||
showEditorTab: false,
|
||||
viewReady: false,
|
||||
eventWarningCount: 0,
|
||||
activeTab: 0,
|
||||
currentName: this.$state.$current.name,
|
||||
isDataValid: true,
|
||||
};
|
||||
|
||||
this.state.activeTab = this.LocalStorage.getActiveTab('configuration');
|
||||
|
||||
this.formValues = new KubernetesConfigurationFormValues();
|
||||
|
||||
this.resourcePools = await this.KubernetesResourcePoolService.get();
|
||||
await this.getConfiguration();
|
||||
await this.getApplications(this.configuration.Namespace);
|
||||
await this.getEvents(this.configuration.Namespace);
|
||||
this.formValues.ResourcePool = _.find(this.resourcePools, (resourcePool) => resourcePool.Namespace.Name === this.configuration.Namespace);
|
||||
this.formValues.Id = this.configuration.Id;
|
||||
this.formValues.Name = this.configuration.Name;
|
||||
this.formValues.Type = this.configuration.Type;
|
||||
this.formValues.Data = _.map(this.configuration.Data, (value, key) => {
|
||||
if (this.configuration.Type === KubernetesConfigurationTypes.SECRET) {
|
||||
value = atob(value);
|
||||
}
|
||||
this.formValues.DataYaml += key + ': ' + value + '\n';
|
||||
const entry = new KubernetesConfigurationFormValuesDataEntry();
|
||||
entry.Key = key;
|
||||
entry.Value = value;
|
||||
return entry;
|
||||
});
|
||||
await this.getConfigurations();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||
} finally {
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
if (this.state.currentName !== this.$state.$current.name) {
|
||||
this.LocalStorage.storeActiveTab('configuration', 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesConfigurationController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesConfigurationController', KubernetesConfigurationController);
|
122
app/kubernetes/views/configure/configure.html
Normal file
122
app/kubernetes/views/configure/configure.html
Normal file
|
@ -0,0 +1,122 @@
|
|||
<kubernetes-view-header title="Kubernetes features configuration" state="portainer.endpoints.endpoint.kubernetesConfig" view-ready="ctrl.state.viewReady">
|
||||
<a ui-sref="portainer.endpoints">Endpoints</a> > <a ui-sref="portainer.endpoints.endpoint({id: ctrl.endpoint.Id})">{{ ctrl.endpoint.Name }}</a> > Kubernetes configuration
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Expose applications over external IP addresses
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
Enabling this feature will allow users to expose application they deploy over an external IP address assigned by cloud provider.
|
||||
<p>
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Ensure that your cloud provider allows you to create load balancers if you want to use this feature. Might incur costs.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Allow users to use external load balancer
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.UseLoadBalancer" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Available storage options
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="!ctrl.storageClassAvailable()">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Unable to detect any storage class available to persist data. Users won't be able to persist application data inside this cluster.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="ctrl.storageClassAvailable()">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
<p>
|
||||
Select which storage options will be available for use when deploying applications. Have a look at your storage driver documentation to figure out which access
|
||||
policy to configure.
|
||||
</p>
|
||||
<p>
|
||||
You can find more information about access modes
|
||||
<a href="https://kubernetes.io/docs/concepts/storage/persistent-volumes/#access-modes" target="_blank">in the official Kubernetes documentation</a>.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="ctrl.storageClassAvailable()">
|
||||
<div style="margin-top: 10px;" class="col-sm-12">
|
||||
<table class="table" style="table-layout: fixed;">
|
||||
<tbody>
|
||||
<tr class="text-muted">
|
||||
<td>Storage</td>
|
||||
<td>Shared access policy</td>
|
||||
</tr>
|
||||
<tr ng-repeat="class in ctrl.StorageClasses">
|
||||
<td>
|
||||
<div style="margin: 5px;">
|
||||
<label class="switch" style="margin-right: 10px;"> <input type="checkbox" ng-model="class.selected" /><i></i> </label>
|
||||
<span>{{ class.Name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
isteven-multi-select
|
||||
input-model="class.availableAccessModes"
|
||||
output-model="class.AccessModes"
|
||||
button-label="Name"
|
||||
item-label="Description"
|
||||
tick-property="selected"
|
||||
directive-id="{{ class.Name }}"
|
||||
helper-elements=""
|
||||
translation="{nothingSelected: 'Not configured'}"
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<span ng-if="!ctrl.hasValidStorageConfiguration()" style="margin-left: 5px;" class="text-muted small">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Shared access policy configuration required
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<!-- actions -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-click="ctrl.configure()"
|
||||
ng-disabled="ctrl.state.actionInProgress || !ctrl.hasValidStorageConfiguration()"
|
||||
button-spinner="ctrl.state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="ctrl.state.actionInProgress">Save configuration</span>
|
||||
<span ng-show="ctrl.state.actionInProgress">Saving configuration...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
115
app/kubernetes/views/configure/configureController.js
Normal file
115
app/kubernetes/views/configure/configureController.js
Normal file
|
@ -0,0 +1,115 @@
|
|||
import _ from 'lodash-es';
|
||||
import angular from 'angular';
|
||||
import { KubernetesStorageClassAccessPolicies, KubernetesStorageClass } from 'Kubernetes/models/storage-class/models';
|
||||
|
||||
class KubernetesConfigureController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, $stateParams, Notifications, KubernetesStorageService, EndpointService, EndpointProvider) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.$stateParams = $stateParams;
|
||||
this.Notifications = Notifications;
|
||||
this.KubernetesStorageService = KubernetesStorageService;
|
||||
this.EndpointService = EndpointService;
|
||||
this.EndpointProvider = EndpointProvider;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.configureAsync = this.configureAsync.bind(this);
|
||||
}
|
||||
|
||||
storageClassAvailable() {
|
||||
return this.StorageClasses && this.StorageClasses.length > 0;
|
||||
}
|
||||
|
||||
hasValidStorageConfiguration() {
|
||||
let valid = true;
|
||||
_.forEach(this.StorageClasses, (item) => {
|
||||
if (item.selected && item.AccessModes.length === 0) {
|
||||
valid = false;
|
||||
}
|
||||
});
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
async configureAsync() {
|
||||
try {
|
||||
this.state.actionInProgress = true;
|
||||
const classes = _.without(
|
||||
_.map(this.StorageClasses, (item) => {
|
||||
if (item.selected) {
|
||||
const res = new KubernetesStorageClass();
|
||||
res.Name = item.Name;
|
||||
res.AccessModes = _.map(item.AccessModes, 'Name');
|
||||
return res;
|
||||
}
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
|
||||
this.endpoint.Kubernetes.Configuration.StorageClasses = classes;
|
||||
this.endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer;
|
||||
await this.EndpointService.updateEndpoint(this.endpoint.Id, this.endpoint);
|
||||
const endpoints = this.EndpointProvider.endpoints();
|
||||
const modifiedEndpoint = _.find(endpoints, (item) => item.Id === this.endpoint.Id);
|
||||
if (modifiedEndpoint) {
|
||||
modifiedEndpoint.Kubernetes.Configuration.StorageClasses = classes;
|
||||
modifiedEndpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer;
|
||||
this.EndpointProvider.setEndpoints(endpoints);
|
||||
}
|
||||
this.Notifications.success('Configuration successfully applied');
|
||||
this.$state.go('portainer.home');
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to apply configuration');
|
||||
} finally {
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
configure() {
|
||||
return this.$async(this.configureAsync);
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
this.state = {
|
||||
actionInProgress: false,
|
||||
displayConfigureClassPanel: {},
|
||||
viewReady: false,
|
||||
};
|
||||
|
||||
this.formValues = {
|
||||
UseLoadBalancer: false,
|
||||
};
|
||||
|
||||
try {
|
||||
const endpointId = this.$stateParams.id;
|
||||
[this.StorageClasses, this.endpoint] = await Promise.all([this.KubernetesStorageService.get(endpointId), this.EndpointService.endpoint(endpointId)]);
|
||||
_.forEach(this.StorageClasses, (item) => {
|
||||
item.availableAccessModes = new KubernetesStorageClassAccessPolicies();
|
||||
const storage = _.find(this.endpoint.Kubernetes.Configuration.StorageClasses, (sc) => sc.Name === item.Name);
|
||||
if (storage) {
|
||||
item.selected = true;
|
||||
_.forEach(storage.AccessModes, (access) => {
|
||||
const mode = _.find(item.availableAccessModes, { Name: access });
|
||||
if (mode) {
|
||||
mode.selected = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.formValues.UseLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve storage classes');
|
||||
} finally {
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesConfigureController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesConfigureController', KubernetesConfigureController);
|
99
app/kubernetes/views/dashboard/dashboard.html
Normal file
99
app/kubernetes/views/dashboard/dashboard.html
Normal file
|
@ -0,0 +1,99 @@
|
|||
<kubernetes-view-header title="Dashboard" state="kubernetes.dashboard" view-ready="ctrl.state.viewReady">
|
||||
Endpoint summary
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<kubernetes-feedback-panel></kubernetes-feedback-panel>
|
||||
|
||||
<div class="row" ng-if="ctrl.endpoint">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tachometer-alt" title-text="Endpoint info"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Endpoint</td>
|
||||
<td>
|
||||
{{ ctrl.endpoint.Name }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>URL</td>
|
||||
<td>{{ ctrl.endpoint.URL | stripprotocol }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tags</td>
|
||||
<td>
|
||||
<span ng-if="ctrl.endpoint.Tags.length === 0">
|
||||
-
|
||||
</span>
|
||||
<span ng-if="ctrl.endpoint.Tags.length > 0">
|
||||
<span ng-repeat="tag in ctrl.endpoint.Tags"> {{ tag }}{{ $last ? '' : ', ' }} </span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-6" ng-if="ctrl.pools">
|
||||
<a ui-sref="kubernetes.resourcePools">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon blue pull-left">
|
||||
<i class="fa fa-layer-group"></i>
|
||||
</div>
|
||||
<div class="title">{{ ctrl.pools.length }}</div>
|
||||
<div class="comment">{{ ctrl.pools.length === 1 ? 'Resource pool' : 'Resource pools' }}</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6" ng-if="ctrl.applications">
|
||||
<a ui-sref="kubernetes.applications">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon blue pull-left">
|
||||
<i class="fa fa-laptop-code"></i>
|
||||
</div>
|
||||
<div class="title">{{ ctrl.applications.length }}</div>
|
||||
<div class="comment">{{ ctrl.applications.length === 1 ? 'Application' : 'Applications' }}</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6" ng-if="ctrl.configurations">
|
||||
<a ui-sref="kubernetes.configurations">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon blue pull-left">
|
||||
<i class="fa fa-file-code"></i>
|
||||
</div>
|
||||
<div class="title">{{ ctrl.configurations.length }}</div>
|
||||
<div class="comment">{{ ctrl.configurations.length === 1 ? 'Configuration' : 'Configurations' }}</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6" ng-if="ctrl.volumes">
|
||||
<a ui-sref="kubernetes.volumes">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon blue pull-left">
|
||||
<i class="fa fa-database"></i>
|
||||
</div>
|
||||
<div class="title">{{ ctrl.volumes.length }}</div>
|
||||
<div class="comment">{{ ctrl.volumes.length === 1 ? 'Volume' : 'Volumes' }}</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
5
app/kubernetes/views/dashboard/dashboard.js
Normal file
5
app/kubernetes/views/dashboard/dashboard.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesDashboardView', {
|
||||
templateUrl: './dashboard.html',
|
||||
controller: 'KubernetesDashboardController',
|
||||
controllerAs: 'ctrl',
|
||||
});
|
88
app/kubernetes/views/dashboard/dashboardController.js
Normal file
88
app/kubernetes/views/dashboard/dashboardController.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
|
||||
|
||||
class KubernetesDashboardController {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$async,
|
||||
Notifications,
|
||||
EndpointService,
|
||||
EndpointProvider,
|
||||
KubernetesResourcePoolService,
|
||||
KubernetesApplicationService,
|
||||
KubernetesConfigurationService,
|
||||
KubernetesVolumeService,
|
||||
KubernetesNamespaceHelper,
|
||||
Authentication
|
||||
) {
|
||||
this.$async = $async;
|
||||
this.Notifications = Notifications;
|
||||
this.EndpointService = EndpointService;
|
||||
this.EndpointProvider = EndpointProvider;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
this.KubernetesConfigurationService = KubernetesConfigurationService;
|
||||
this.KubernetesVolumeService = KubernetesVolumeService;
|
||||
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
|
||||
this.Authentication = Authentication;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.getAll = this.getAll.bind(this);
|
||||
this.getAllAsync = this.getAllAsync.bind(this);
|
||||
}
|
||||
|
||||
async getAllAsync() {
|
||||
const isAdmin = this.Authentication.isAdmin();
|
||||
|
||||
try {
|
||||
const endpointId = this.EndpointProvider.endpointID();
|
||||
const [endpoint, pools, applications, configurations, volumes] = await Promise.all([
|
||||
this.EndpointService.endpoint(endpointId),
|
||||
this.KubernetesResourcePoolService.get(),
|
||||
this.KubernetesApplicationService.get(),
|
||||
this.KubernetesConfigurationService.get(),
|
||||
this.KubernetesVolumeService.get(),
|
||||
]);
|
||||
this.endpoint = endpoint;
|
||||
this.applications = applications;
|
||||
this.volumes = volumes;
|
||||
|
||||
if (!isAdmin) {
|
||||
this.pools = _.filter(pools, (pool) => {
|
||||
return !this.KubernetesNamespaceHelper.isSystemNamespace(pool.Namespace.Name);
|
||||
});
|
||||
|
||||
this.configurations = _.filter(configurations, (config) => {
|
||||
return !KubernetesConfigurationHelper.isSystemToken(config);
|
||||
});
|
||||
} else {
|
||||
this.pools = pools;
|
||||
this.configurations = configurations;
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load dashboard data');
|
||||
}
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return this.$async(this.getAllAsync);
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
this.state = {
|
||||
viewReady: false,
|
||||
};
|
||||
|
||||
await this.getAll();
|
||||
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesDashboardController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesDashboardController', KubernetesDashboardController);
|
118
app/kubernetes/views/deploy/deploy.html
Normal file
118
app/kubernetes/views/deploy/deploy.html
Normal file
|
@ -0,0 +1,118 @@
|
|||
<kubernetes-view-header title="Advanced deployment" state="kubernetes.deploy" view-ready="ctrl.state.viewReady">
|
||||
Deploy Kubernetes resources
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<kubernetes-feedback-panel></kubernetes-feedback-panel>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<uib-tabset active="ctrl.state.activeTab" justified="true" type="pills">
|
||||
<uib-tab index="0">
|
||||
<uib-tab-heading> <i class="fa fa-code space-right" aria-hidden="true"></i> Deploy </uib-tab-heading>
|
||||
|
||||
<form class="form-horizontal" style="margin-top: 20px;">
|
||||
<div class="form-group">
|
||||
<label for="target_node" class="col-lg-1 col-sm-2 control-label text-left">Resource pool</label>
|
||||
<div class="col-lg-11 col-sm-10">
|
||||
<select class="form-control" ng-model="ctrl.formValues.Namespace" ng-options="namespace.Name as namespace.Name for namespace in ctrl.namespaces"></select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- deploy-type -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Deployment type
|
||||
</div>
|
||||
<div class="form-group"></div>
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<div class="boxselector_wrapper">
|
||||
<div>
|
||||
<input type="radio" id="method_kubernetes" ng-model="ctrl.state.DeployType" ng-value="ctrl.ManifestDeployTypes.KUBERNETES" />
|
||||
<label for="method_kubernetes">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-cubes" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Kubernetes
|
||||
</div>
|
||||
<p>Kubernetes manifest format</p>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="method_compose" ng-model="ctrl.state.DeployType" ng-value="ctrl.ManifestDeployTypes.COMPOSE" />
|
||||
<label for="method_compose">
|
||||
<div class="boxselector_header">
|
||||
<i class="fab fa-docker" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Compose
|
||||
</div>
|
||||
<p>docker-compose format</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !deploy-type -->
|
||||
<!-- editor -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Web editor
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small" ng-show="ctrl.state.DeployType === ctrl.ManifestDeployTypes.COMPOSE">
|
||||
<p>
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Portainer uses <a href="https://kompose.io/" target="_blank">Kompose</a> to convert your Compose manifest to a Kubernetes compliant manifest. Be wary that not
|
||||
all the Compose format options are supported by Kompose at the moment.
|
||||
</p>
|
||||
<p>
|
||||
You can get more information about Compose file format in the
|
||||
<a href="https://docs.docker.com/compose/compose-file/" target="_blank">official documentation</a>.
|
||||
</p>
|
||||
</span>
|
||||
<span class="col-sm-12 text-muted small" ng-show="ctrl.state.DeployType === ctrl.ManifestDeployTypes.KUBERNETES">
|
||||
You can get more information about Kubernetes file format in the
|
||||
<a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/" target="_blank">official documentation</a>.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<code-editor
|
||||
identifier="kubernetes-deploy-editor"
|
||||
placeholder="# Define or paste the content of your manifest file here"
|
||||
yml="false"
|
||||
on-change="(ctrl.editorUpdate)"
|
||||
></code-editor>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !editor -->
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="ctrl.disableDeploy()" ng-click="ctrl.deploy()" button-spinner="ctrl.state.actionInProgress">
|
||||
<span ng-hide="ctrl.state.actionInProgress">Deploy</span>
|
||||
<span ng-show="ctrl.state.actionInProgress">Deployment in progress...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="1" disable="ctrl.state.tabLogsDisabled">
|
||||
<uib-tab-heading> <i class="fa fa-file space-right" aria-hidden="true"></i> Logs </uib-tab-heading>
|
||||
<form class="form-horizontal" style="margin-top: 20px;">
|
||||
<div class="form-group" ng-if="ctrl.state.activeTab === 1">
|
||||
<div class="col-sm-12">
|
||||
<code-editor identifier="kubernetes-deploy-logs" read-only="true" yml="false" value="ctrl.errorLog"></code-editor>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
5
app/kubernetes/views/deploy/deploy.js
Normal file
5
app/kubernetes/views/deploy/deploy.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesDeployView', {
|
||||
templateUrl: './deploy.html',
|
||||
controller: 'KubernetesDeployController',
|
||||
controllerAs: 'ctrl',
|
||||
});
|
99
app/kubernetes/views/deploy/deployController.js
Normal file
99
app/kubernetes/views/deploy/deployController.js
Normal file
|
@ -0,0 +1,99 @@
|
|||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import { KubernetesDeployManifestTypes } from 'Kubernetes/models/deploy';
|
||||
|
||||
class KubernetesDeployController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, Notifications, EndpointProvider, KubernetesResourcePoolService, StackService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.Notifications = Notifications;
|
||||
this.EndpointProvider = EndpointProvider;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
this.StackService = StackService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.deployAsync = this.deployAsync.bind(this);
|
||||
this.editorUpdate = this.editorUpdate.bind(this);
|
||||
this.editorUpdateAsync = this.editorUpdateAsync.bind(this);
|
||||
this.getNamespacesAsync = this.getNamespacesAsync.bind(this);
|
||||
}
|
||||
|
||||
disableDeploy() {
|
||||
return _.isEmpty(this.formValues.EditorContent) || _.isEmpty(this.formValues.Namespace) || this.state.actionInProgress;
|
||||
}
|
||||
|
||||
async editorUpdateAsync(cm) {
|
||||
this.formValues.EditorContent = cm.getValue();
|
||||
}
|
||||
|
||||
editorUpdate(cm) {
|
||||
return this.$async(this.editorUpdateAsync, cm);
|
||||
}
|
||||
|
||||
displayErrorLog(log) {
|
||||
this.errorLog = stripAnsi(log);
|
||||
this.state.tabLogsDisabled = false;
|
||||
this.state.activeTab = 1;
|
||||
}
|
||||
|
||||
async deployAsync() {
|
||||
this.errorLog = '';
|
||||
this.state.actionInProgress = true;
|
||||
|
||||
try {
|
||||
const compose = this.state.DeployType === this.ManifestDeployTypes.COMPOSE;
|
||||
await this.StackService.kubernetesDeploy(this.endpointId, this.formValues.Namespace, this.formValues.EditorContent, compose);
|
||||
this.Notifications.success('Manifest successfully deployed');
|
||||
this.$state.go('kubernetes.applications');
|
||||
} catch (err) {
|
||||
this.Notifications.error('Unable to deploy manifest', err, 'Unable to deploy resources');
|
||||
this.displayErrorLog(err.err.data.details);
|
||||
} finally {
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
deploy() {
|
||||
return this.$async(this.deployAsync);
|
||||
}
|
||||
|
||||
async getNamespacesAsync() {
|
||||
try {
|
||||
const pools = await this.KubernetesResourcePoolService.get();
|
||||
this.namespaces = _.map(pools, 'Namespace');
|
||||
this.formValues.Namespace = this.namespaces[0].Name;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load resource pools data');
|
||||
}
|
||||
}
|
||||
|
||||
getNamespaces() {
|
||||
return this.$async(this.getNamespacesAsync);
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
this.state = {
|
||||
DeployType: KubernetesDeployManifestTypes.KUBERNETES,
|
||||
tabLogsDisabled: true,
|
||||
activeTab: 0,
|
||||
viewReady: false,
|
||||
};
|
||||
|
||||
this.formValues = {};
|
||||
this.ManifestDeployTypes = KubernetesDeployManifestTypes;
|
||||
this.endpointId = this.EndpointProvider.endpointID();
|
||||
|
||||
await this.getNamespaces();
|
||||
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesDeployController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesDeployController', KubernetesDeployController);
|
|
@ -0,0 +1,116 @@
|
|||
<kubernetes-view-header title="Resource pool access management" state="kubernetes.resourcePools.resourcePool.access" view-ready="ctrl.state.viewReady">
|
||||
<a ui-sref="kubernetes.resourcePools">Resource pools</a> >
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({id: ctrl.pool.Namespace.Name})">{{ ctrl.pool.Namespace.Name }}</a> > Access management
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row" ng-if="ctrl.pool">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-plug" title-text="Resource pool"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
{{ ctrl.pool.Namespace.Name }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget ng-if="ctrl.availableUsersAndTeams">
|
||||
<rd-widget-header icon="fa-user-lock" title-text="Create access"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Select user(s) and/or team(s)
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-4">
|
||||
<span class="small text-muted" ng-if="ctrl.availableUsersAndTeams.length === 0">
|
||||
No user nor team access has been set on the endpoint. Head over to the
|
||||
<a ui-sref="portainer.endpoints.endpoint.access({id: ctrl.endpointId})">endpoint access view</a> to manage them.
|
||||
</span>
|
||||
<span
|
||||
isteven-multi-select
|
||||
ng-if="ctrl.availableUsersAndTeams.length > 0"
|
||||
input-model="ctrl.availableUsersAndTeams"
|
||||
output-model="ctrl.formValues.multiselectOutput"
|
||||
button-label="icon '-' Name"
|
||||
item-label="icon '-' Name"
|
||||
tick-property="ticked"
|
||||
helper-elements="filter"
|
||||
search-property="Name"
|
||||
translation="{nothingSelected: 'Select one or more users and/or teams', search: 'Search...'}"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KEEP FOR FUTURE RBAC EVOL -->
|
||||
<!-- <div class="form-group" ng-if="ctrl.entityType !== 'registry'">
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Role
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-4">
|
||||
<select ng-if="ctrl.rbacEnabled" class="form-control" ng-model="ctrl.formValues.selectedRole"
|
||||
ng-options="role.Name for role in ctrl.roles">
|
||||
</select>
|
||||
<span class="small text-muted" ng-if="!ctrl.rbacEnabled">
|
||||
The <a ui-sref="portainer.extensions.extension({id: 3})">Role-Based Access Control extension</a> is required to select a specific role.
|
||||
</span>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- actions -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="(ctrl.availableUsersAndTeams | filter:{ticked:true}).length === 0 || ctrl.actionInProgress"
|
||||
ng-click="ctrl.authorizeAccess()"
|
||||
button-spinner="ctrl.actionInProgress"
|
||||
>
|
||||
<span ng-hide="ctrl.state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Create access</span>
|
||||
<span ng-show="ctrl.state.actionInProgress">Creating access...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<!--
|
||||
rbac-enabled="ctrl.rbacEnabled && ctrl.entityType !== 'registry'"
|
||||
roles="ctrl.roles"
|
||||
update-action="ctrl.updateAction"
|
||||
-->
|
||||
<access-datatable
|
||||
ng-if="ctrl.authorizedUsersAndTeams"
|
||||
title-text="Access"
|
||||
title-icon="fa-user-lock"
|
||||
table-key="kubernetes_resourcepool_access"
|
||||
order-by="Name"
|
||||
dataset="ctrl.authorizedUsersAndTeams"
|
||||
remove-action="ctrl.unauthorizeAccess"
|
||||
>
|
||||
</access-datatable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,8 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesResourcePoolAccessView', {
|
||||
templateUrl: './resourcePoolAccess.html',
|
||||
controller: 'KubernetesResourcePoolAccessController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
},
|
||||
});
|
|
@ -0,0 +1,138 @@
|
|||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import { KubernetesPortainerConfigMapConfigName, KubernetesPortainerConfigMapNamespace, KubernetesPortainerConfigMapAccessKey } from 'Kubernetes/models/config-map/models';
|
||||
import { UserAccessViewModel, TeamAccessViewModel } from 'Portainer/models/access';
|
||||
import KubernetesConfigMapHelper from 'Kubernetes/helpers/configMapHelper';
|
||||
|
||||
class KubernetesResourcePoolAccessController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, Notifications, KubernetesResourcePoolService, KubernetesConfigMapService, EndpointProvider, EndpointService, GroupService, AccessService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.Notifications = Notifications;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
this.KubernetesConfigMapService = KubernetesConfigMapService;
|
||||
|
||||
this.EndpointProvider = EndpointProvider;
|
||||
this.EndpointService = EndpointService;
|
||||
this.GroupService = GroupService;
|
||||
this.AccessService = AccessService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.authorizeAccessAsync = this.authorizeAccessAsync.bind(this);
|
||||
this.unauthorizeAccessAsync = this.unauthorizeAccessAsync.bind(this);
|
||||
|
||||
this.unauthorizeAccess = this.unauthorizeAccess.bind(this);
|
||||
}
|
||||
|
||||
initAccessConfigMap(configMap) {
|
||||
configMap.Name = KubernetesPortainerConfigMapConfigName;
|
||||
configMap.Namespace = KubernetesPortainerConfigMapNamespace;
|
||||
configMap.Data[KubernetesPortainerConfigMapAccessKey] = {};
|
||||
return configMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init
|
||||
*/
|
||||
// TODO: refactor: roles need to be fetched if RBAC is activated on Portainer
|
||||
// see porAccessManagementController for more details
|
||||
// Extract the fetching code and merge it in AccessService.accesses() function
|
||||
async onInit() {
|
||||
this.state = {
|
||||
actionInProgress: false,
|
||||
viewReady: false,
|
||||
};
|
||||
|
||||
this.formValues = {
|
||||
multiselectOutput: [],
|
||||
};
|
||||
|
||||
this.endpointId = this.EndpointProvider.endpointID();
|
||||
|
||||
try {
|
||||
const name = this.$transition$.params().id;
|
||||
let [endpoint, pool, configMap] = await Promise.all([
|
||||
this.EndpointService.endpoint(this.endpointId),
|
||||
this.KubernetesResourcePoolService.get(name),
|
||||
this.KubernetesConfigMapService.get(KubernetesPortainerConfigMapNamespace, KubernetesPortainerConfigMapConfigName),
|
||||
]);
|
||||
const group = await this.GroupService.group(endpoint.GroupId);
|
||||
const roles = [];
|
||||
const endpointAccesses = await this.AccessService.accesses(endpoint, group, roles);
|
||||
this.pool = pool;
|
||||
if (configMap.Id === 0) {
|
||||
configMap = this.initAccessConfigMap(configMap);
|
||||
}
|
||||
configMap = KubernetesConfigMapHelper.parseJSONData(configMap);
|
||||
|
||||
this.authorizedUsersAndTeams = [];
|
||||
this.accessConfigMap = configMap;
|
||||
const poolAccesses = configMap.Data[KubernetesPortainerConfigMapAccessKey][name];
|
||||
if (poolAccesses) {
|
||||
this.authorizedUsersAndTeams = _.filter(endpointAccesses.authorizedUsersAndTeams, (item) => {
|
||||
if (item instanceof UserAccessViewModel && poolAccesses.UserAccessPolicies) {
|
||||
return poolAccesses.UserAccessPolicies[item.Id] !== undefined;
|
||||
} else if (item instanceof TeamAccessViewModel && poolAccesses.TeamAccessPolicies) {
|
||||
return poolAccesses.TeamAccessPolicies[item.Id] !== undefined;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
this.availableUsersAndTeams = _.without(endpointAccesses.authorizedUsersAndTeams, ...this.authorizedUsersAndTeams);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve resource pool information');
|
||||
} finally {
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorize access
|
||||
*/
|
||||
async authorizeAccessAsync() {
|
||||
try {
|
||||
this.state.actionInProgress = true;
|
||||
const newAccesses = _.concat(this.authorizedUsersAndTeams, this.formValues.multiselectOutput);
|
||||
const accessConfigMap = KubernetesConfigMapHelper.modifiyNamespaceAccesses(angular.copy(this.accessConfigMap), this.pool.Namespace.Name, newAccesses);
|
||||
await this.KubernetesConfigMapService.update(accessConfigMap);
|
||||
this.Notifications.success('Access successfully created');
|
||||
this.$state.reload();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to create accesses');
|
||||
}
|
||||
}
|
||||
|
||||
authorizeAccess() {
|
||||
return this.$async(this.authorizeAccessAsync);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
async unauthorizeAccessAsync(selectedItems) {
|
||||
try {
|
||||
this.state.actionInProgress = true;
|
||||
const newAccesses = _.without(this.authorizedUsersAndTeams, ...selectedItems);
|
||||
const accessConfigMap = KubernetesConfigMapHelper.modifiyNamespaceAccesses(angular.copy(this.accessConfigMap), this.pool.Namespace.Name, newAccesses);
|
||||
await this.KubernetesConfigMapService.update(accessConfigMap);
|
||||
this.Notifications.success('Access successfully removed');
|
||||
this.$state.reload();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to remove accesses');
|
||||
} finally {
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
unauthorizeAccess(selectedItems) {
|
||||
return this.$async(this.unauthorizeAccessAsync, selectedItems);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesResourcePoolAccessController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesResourcePoolAccessController', KubernetesResourcePoolAccessController);
|
|
@ -0,0 +1,163 @@
|
|||
<kubernetes-view-header title="Create a resource pool" state="kubernetes.resourcePools.new" view-ready="ctrl.state.viewReady">
|
||||
<a ui-sref="kubernetes.resourcePools">Resource pools</a> > Create a resource pool
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" autocomplete="off" name="resourcePoolCreationForm">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="pool_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="pool_name"
|
||||
ng-model="ctrl.formValues.Name"
|
||||
ng-pattern="/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/"
|
||||
ng-change="ctrl.onChangeName()"
|
||||
placeholder="my-project"
|
||||
required
|
||||
auto-focus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="resourcePoolCreationForm.pool_name.$invalid || ctrl.state.isAlreadyExist">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="resourcePoolCreationForm.pool_name.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
<p ng-message="pattern"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist of lower case alphanumeric characters, '-' or '.', and must start and end
|
||||
with an alphanumeric character.</p
|
||||
>
|
||||
</div>
|
||||
<p ng-if="ctrl.state.isAlreadyExist"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A resource pool with the same name already exists.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Quota
|
||||
</div>
|
||||
<!-- quotas-switch -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<p>
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
A resource pool segments the underyling physical Kubernetes cluster into smaller virtual clusters. You should assign a capped limit of resources to this pool or
|
||||
disable for the safe operation of your platform.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Resource assignment
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.hasQuota" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="ctrl.formValues.hasQuota && !ctrl.isQuotaValid()">
|
||||
<span class="col-sm-12 text-warning small">
|
||||
<p> <i class="fa fa-exclamation-triangle" aria-hidden="true" style="margin-right: 2px;"></i> At least a single limit must be set for the quota to be valid. </p>
|
||||
</span>
|
||||
</div>
|
||||
<!-- !quotas-switch -->
|
||||
<div ng-if="ctrl.formValues.hasQuota">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Resource limits
|
||||
</div>
|
||||
<div>
|
||||
<!-- memory-limit-input -->
|
||||
<div class="form-group">
|
||||
<label for="memory-limit" class="col-sm-3 col-lg-2 control-label text-left" style="margin-top: 20px;">
|
||||
Memory
|
||||
</label>
|
||||
<div class="col-sm-3">
|
||||
<slider
|
||||
model="ctrl.formValues.MemoryLimit"
|
||||
floor="ctrl.defaults.MemoryLimit"
|
||||
ceil="ctrl.state.sliderMaxMemory"
|
||||
step="128"
|
||||
ng-if="ctrl.state.sliderMaxMemory"
|
||||
></slider>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<input
|
||||
name="memory_limit"
|
||||
type="number"
|
||||
min="{{ ctrl.defaults.MemoryLimit }}"
|
||||
max="{{ ctrl.state.sliderMaxMemory }}"
|
||||
class="form-control"
|
||||
ng-model="ctrl.formValues.MemoryLimit"
|
||||
id="memory-limit"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<p class="small text-muted" style="margin-top: 7px;">
|
||||
Maximum memory usage (<b>MB</b>)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="resourcePoolCreationForm.memory_limit.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="resourcePoolCreationForm.pool_name.$error">
|
||||
<p
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Value must be between {{ ctrl.defaults.MemoryLimit }} and {{ ctrl.state.sliderMaxMemory }}</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !memory-limit-input -->
|
||||
<!-- cpu-limit-input -->
|
||||
<div class="form-group">
|
||||
<label for="cpu-limit" class="col-sm-3 col-lg-2 control-label text-left" style="margin-top: 20px;">
|
||||
CPU
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
<slider
|
||||
model="ctrl.formValues.CpuLimit"
|
||||
floor="ctrl.defaults.CpuLimit"
|
||||
ceil="ctrl.state.sliderMaxCpu"
|
||||
step="0.1"
|
||||
precision="2"
|
||||
ng-if="ctrl.state.sliderMaxCpu"
|
||||
></slider>
|
||||
</div>
|
||||
<div class="col-sm-4" style="margin-top: 20px;">
|
||||
<p class="small text-muted">
|
||||
Maximum CPU usage
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !cpu-limit-input -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="!resourcePoolCreationForm.$valid || ctrl.state.actionInProgress || (ctrl.formValues.hasQuota && !ctrl.isQuotaValid()) || !ctrl.isValid()"
|
||||
ng-click="ctrl.createResourcePool()"
|
||||
button-spinner="ctrl.state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="ctrl.state.actionInProgress">Create resource pool</span>
|
||||
<span ng-show="ctrl.state.actionInProgress">Creation in progress...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,5 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesCreateResourcePoolView', {
|
||||
templateUrl: './createResourcePool.html',
|
||||
controller: 'KubernetesCreateResourcePoolController',
|
||||
controllerAs: 'ctrl',
|
||||
});
|
|
@ -0,0 +1,127 @@
|
|||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import filesizeParser from 'filesize-parser';
|
||||
import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
|
||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||
|
||||
class KubernetesCreateResourcePoolController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, Notifications, KubernetesNodeService, KubernetesResourcePoolService, Authentication) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.Notifications = Notifications;
|
||||
this.Authentication = Authentication;
|
||||
|
||||
this.KubernetesNodeService = KubernetesNodeService;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.createResourcePoolAsync = this.createResourcePoolAsync.bind(this);
|
||||
this.getResourcePoolsAsync = this.getResourcePoolsAsync.bind(this);
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return !this.state.isAlreadyExist;
|
||||
}
|
||||
|
||||
onChangeName() {
|
||||
this.state.isAlreadyExist = _.find(this.resourcePools, (resourcePool) => resourcePool.Namespace.Name === this.formValues.Name) !== undefined;
|
||||
}
|
||||
|
||||
isQuotaValid() {
|
||||
if (
|
||||
this.state.sliderMaxCpu < this.formValues.CpuLimit ||
|
||||
this.state.sliderMaxMemory < this.formValues.MemoryLimit ||
|
||||
(this.formValues.CpuLimit === 0 && this.formValues.MemoryLimit === 0)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
checkDefaults() {
|
||||
if (this.formValues.CpuLimit < this.defaults.CpuLimit) {
|
||||
this.formValues.CpuLimit = this.defaults.CpuLimit;
|
||||
}
|
||||
if (this.formValues.MemoryLimit < KubernetesResourceReservationHelper.megaBytesValue(this.defaults.MemoryLimit)) {
|
||||
this.formValues.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(this.defaults.MemoryLimit);
|
||||
}
|
||||
}
|
||||
|
||||
async createResourcePoolAsync() {
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
this.checkDefaults();
|
||||
const owner = this.Authentication.getUserDetails().username;
|
||||
await this.KubernetesResourcePoolService.create(
|
||||
this.formValues.Name,
|
||||
owner,
|
||||
this.formValues.hasQuota,
|
||||
this.formValues.CpuLimit,
|
||||
KubernetesResourceReservationHelper.bytesValue(this.formValues.MemoryLimit)
|
||||
);
|
||||
this.Notifications.success('Resource pool successfully created', this.formValues.Name);
|
||||
this.$state.go('kubernetes.resourcePools');
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to create resource pool');
|
||||
} finally {
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
createResourcePool() {
|
||||
return this.$async(this.createResourcePoolAsync);
|
||||
}
|
||||
|
||||
async getResourcePoolsAsync() {
|
||||
try {
|
||||
this.resourcePools = await this.KubernetesResourcePoolService.get();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve resource pools');
|
||||
}
|
||||
}
|
||||
|
||||
getResourcePools() {
|
||||
return this.$async(this.getResourcePoolsAsync);
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
try {
|
||||
this.defaults = KubernetesResourceQuotaDefaults;
|
||||
|
||||
this.formValues = {
|
||||
MemoryLimit: this.defaults.MemoryLimit,
|
||||
CpuLimit: this.defaults.CpuLimit,
|
||||
hasQuota: true,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
actionInProgress: false,
|
||||
sliderMaxMemory: 0,
|
||||
sliderMaxCpu: 0,
|
||||
viewReady: false,
|
||||
isAlreadyExist: false,
|
||||
};
|
||||
|
||||
const nodes = await this.KubernetesNodeService.get();
|
||||
|
||||
_.forEach(nodes, (item) => {
|
||||
this.state.sliderMaxMemory += filesizeParser(item.Memory);
|
||||
this.state.sliderMaxCpu += item.CPU;
|
||||
});
|
||||
this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory);
|
||||
await this.getResourcePools();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||
} finally {
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesCreateResourcePoolController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesCreateResourcePoolController', KubernetesCreateResourcePoolController);
|
189
app/kubernetes/views/resource-pools/edit/resourcePool.html
Normal file
189
app/kubernetes/views/resource-pools/edit/resourcePool.html
Normal file
|
@ -0,0 +1,189 @@
|
|||
<kubernetes-view-header title="Resource pool details" state="kubernetes.resourcePools.resourcePool" view-ready="ctrl.state.viewReady">
|
||||
<a ui-sref="kubernetes.resourcePools">Resource pools</a> > {{ ctrl.pool.Namespace.Name }}
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<uib-tabset active="ctrl.state.activeTab" justified="true" type="pills">
|
||||
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)">
|
||||
<uib-tab-heading> <i class="fa fa-layer-group space-right" aria-hidden="true"></i> Resource pool </uib-tab-heading>
|
||||
<form class="form-horizontal" autocomplete="off" name="resourcePoolEditForm" style="padding: 20px; margin-top: 10px;">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="pool_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" name="pool_name" ng-model="ctrl.pool.Namespace.Name" disabled />
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<div ng-if="ctrl.isAdmin" class="col-sm-12 form-section-title">Quota</div>
|
||||
<!-- quotas-switch -->
|
||||
<div ng-if="ctrl.isAdmin" class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Resource assignment
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.hasQuota" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="ctrl.formValues.hasQuota && !ctrl.isQuotaValid()">
|
||||
<span class="col-sm-12 text-warning small">
|
||||
<p> <i class="fa fa-exclamation-triangle" aria-hidden="true" style="margin-right: 2px;"></i> At least a single limit must be set for the quota to be valid. </p>
|
||||
</span>
|
||||
</div>
|
||||
<!-- !quotas-switch -->
|
||||
<div ng-if="ctrl.formValues.hasQuota && ctrl.isAdmin">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Resource limits
|
||||
</div>
|
||||
<div>
|
||||
<!-- memory-limit-input -->
|
||||
<div class="form-group">
|
||||
<label for="memory-limit" class="col-sm-3 col-lg-2 control-label text-left" style="margin-top: 20px;">
|
||||
Memory limit
|
||||
</label>
|
||||
<div class="col-sm-3">
|
||||
<slider
|
||||
model="ctrl.formValues.MemoryLimit"
|
||||
floor="ctrl.defaults.MemoryLimit"
|
||||
ceil="ctrl.state.sliderMaxMemory"
|
||||
step="128"
|
||||
ng-if="ctrl.state.sliderMaxMemory"
|
||||
></slider>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<input
|
||||
name="memory_limit"
|
||||
type="number"
|
||||
min="{{ ctrl.defaults.MemoryLimit }}"
|
||||
max="{{ ctrl.state.sliderMaxMemory }}"
|
||||
class="form-control"
|
||||
ng-model="ctrl.formValues.MemoryLimit"
|
||||
id="memory-limit"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<p class="small text-muted" style="margin-top: 7px;">
|
||||
Memory limit (<b>MB</b>)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="resourcePoolEditForm.memory_limit.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="resourcePoolEditForm.pool_name.$error">
|
||||
<p
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Value must be between {{ ctrl.defaults.MemoryLimit }} and
|
||||
{{ ctrl.state.sliderMaxMemory }}</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !memory-limit-input -->
|
||||
<!-- cpu-limit-input -->
|
||||
<div class="form-group">
|
||||
<label for="cpu-limit" class="col-sm-3 col-lg-2 control-label text-left" style="margin-top: 20px;">
|
||||
CPU limit
|
||||
</label>
|
||||
<div class="col-sm-5">
|
||||
<slider
|
||||
model="ctrl.formValues.CpuLimit"
|
||||
floor="ctrl.defaults.CpuLimit"
|
||||
ceil="ctrl.state.sliderMaxCpu"
|
||||
step="0.1"
|
||||
precision="2"
|
||||
ng-if="ctrl.state.sliderMaxCpu"
|
||||
></slider>
|
||||
</div>
|
||||
<div class="col-sm-4" style="margin-top: 20px;">
|
||||
<p class="small text-muted">
|
||||
Maximum CPU usage
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !cpu-limit-input -->
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="ctrl.formValues.hasQuota">
|
||||
<kubernetes-resource-reservation
|
||||
ng-if="ctrl.pool.Quota"
|
||||
description="Resource reservation represents the total amount of resource assigned to all the applications deployed inside this resource pool."
|
||||
cpu="ctrl.state.cpuUsed"
|
||||
memory="ctrl.state.memoryUsed"
|
||||
cpu-limit="ctrl.formValues.CpuLimit"
|
||||
memory-limit="ctrl.formValues.MemoryLimit"
|
||||
>
|
||||
</kubernetes-resource-reservation>
|
||||
</div>
|
||||
<!-- actions -->
|
||||
<div ng-if="ctrl.isAdmin" class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<div ng-if="ctrl.isAdmin" class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="!resourcePoolEditForm.$valid || ctrl.state.actionInProgress || (ctrl.formValues.hasQuota && !ctrl.isQuotaValid())"
|
||||
ng-click="ctrl.updateResourcePool()"
|
||||
button-spinner="ctrl.state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="ctrl.state.actionInProgress">Update resource pool</span>
|
||||
<span ng-show="ctrl.state.actionInProgress">Update in progress...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</uib-tab>
|
||||
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)">
|
||||
<uib-tab-heading>
|
||||
<i class="fa fa-history space-right" aria-hidden="true"></i> Events
|
||||
<div ng-if="ctrl.hasEventWarnings()">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
{{ ctrl.state.eventWarningCount }} warning(s)
|
||||
</div>
|
||||
</uib-tab-heading>
|
||||
<kubernetes-events-datatable
|
||||
title-text="Events"
|
||||
title-icon="fa-history"
|
||||
dataset="ctrl.events"
|
||||
table-key="kubernetes.resourcepool.events"
|
||||
order-by="Date"
|
||||
reverse-order="true"
|
||||
loading="ctrl.state.eventsLoading"
|
||||
refresh-callback="ctrl.getEvents"
|
||||
></kubernetes-events-datatable>
|
||||
</uib-tab>
|
||||
<uib-tab index="2" ng-if="ctrl.pool.Yaml" select="ctrl.showEditor()" classes="btn-sm">
|
||||
<uib-tab-heading> <i class="fa fa-code space-right" aria-hidden="true"></i> YAML </uib-tab-heading>
|
||||
<div style="padding-right: 25px;" ng-if="ctrl.state.showEditorTab">
|
||||
<kubernetes-yaml-inspector key="resource-pool-yaml" data="ctrl.pool.Yaml"></kubernetes-yaml-inspector>
|
||||
</div>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="ctrl.applications && ctrl.applications.length > 0">
|
||||
<div class="col-sm-12">
|
||||
<kubernetes-resource-pool-applications-datatable
|
||||
dataset="ctrl.applications"
|
||||
table-key="kubernetes.resourcepool.applications"
|
||||
order-by="Name"
|
||||
refresh-callback="ctrl.getApplications"
|
||||
loading="ctrl.state.applicationsLoading"
|
||||
title-text="Applications running in this resource pool"
|
||||
title-icon="fa-laptop-code"
|
||||
>
|
||||
</kubernetes-resource-pool-applications-datatable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
8
app/kubernetes/views/resource-pools/edit/resourcePool.js
Normal file
8
app/kubernetes/views/resource-pools/edit/resourcePool.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesResourcePoolView', {
|
||||
templateUrl: './resourcePool.html',
|
||||
controller: 'KubernetesResourcePoolController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
},
|
||||
});
|
|
@ -0,0 +1,230 @@
|
|||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import filesizeParser from 'filesize-parser';
|
||||
import { KubernetesResourceQuotaDefaults, KubernetesResourceQuota } from 'Kubernetes/models/resource-quota/models';
|
||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
||||
|
||||
class KubernetesResourcePoolController {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$async,
|
||||
$state,
|
||||
Authentication,
|
||||
Notifications,
|
||||
LocalStorage,
|
||||
KubernetesNodeService,
|
||||
KubernetesResourceQuotaService,
|
||||
KubernetesResourcePoolService,
|
||||
KubernetesEventService,
|
||||
KubernetesPodService,
|
||||
KubernetesApplicationService
|
||||
) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.Notifications = Notifications;
|
||||
this.Authentication = Authentication;
|
||||
this.LocalStorage = LocalStorage;
|
||||
|
||||
this.KubernetesNodeService = KubernetesNodeService;
|
||||
this.KubernetesResourceQuotaService = KubernetesResourceQuotaService;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
this.KubernetesEventService = KubernetesEventService;
|
||||
this.KubernetesPodService = KubernetesPodService;
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.createResourceQuotaAsync = this.createResourceQuotaAsync.bind(this);
|
||||
this.updateResourcePoolAsync = this.updateResourcePoolAsync.bind(this);
|
||||
this.getEvents = this.getEvents.bind(this);
|
||||
this.getEventsAsync = this.getEventsAsync.bind(this);
|
||||
this.getApplicationsAsync = this.getApplicationsAsync.bind(this);
|
||||
}
|
||||
|
||||
selectTab(index) {
|
||||
this.LocalStorage.storeActiveTab('resourcePool', index);
|
||||
}
|
||||
|
||||
isQuotaValid() {
|
||||
if (
|
||||
this.state.sliderMaxCpu < this.formValues.CpuLimit ||
|
||||
this.state.sliderMaxMemory < this.formValues.MemoryLimit ||
|
||||
(this.formValues.CpuLimit === 0 && this.formValues.MemoryLimit === 0)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
checkDefaults() {
|
||||
if (this.formValues.CpuLimit < this.defaults.CpuLimit) {
|
||||
this.formValues.CpuLimit = this.defaults.CpuLimit;
|
||||
}
|
||||
if (this.formValues.MemoryLimit < KubernetesResourceReservationHelper.megaBytesValue(this.defaults.MemoryLimit)) {
|
||||
this.formValues.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(this.defaults.MemoryLimit);
|
||||
}
|
||||
}
|
||||
|
||||
showEditor() {
|
||||
this.state.showEditorTab = true;
|
||||
this.selectTab(2);
|
||||
}
|
||||
|
||||
async createResourceQuotaAsync(namespace, owner, cpuLimit, memoryLimit) {
|
||||
const quota = new KubernetesResourceQuota(namespace);
|
||||
quota.CpuLimit = cpuLimit;
|
||||
quota.MemoryLimit = memoryLimit;
|
||||
quota.ResourcePoolName = namespace;
|
||||
quota.ResourcePoolOwner = owner;
|
||||
await this.KubernetesResourceQuotaService.create(quota);
|
||||
}
|
||||
|
||||
async updateResourcePoolAsync() {
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
this.checkDefaults();
|
||||
const namespace = this.pool.Namespace.Name;
|
||||
const cpuLimit = this.formValues.CpuLimit;
|
||||
const memoryLimit = KubernetesResourceReservationHelper.bytesValue(this.formValues.MemoryLimit);
|
||||
const owner = this.pool.Namespace.ResourcePoolOwner;
|
||||
const quota = this.pool.Quota;
|
||||
|
||||
if (this.formValues.hasQuota) {
|
||||
if (quota) {
|
||||
quota.CpuLimit = cpuLimit;
|
||||
quota.MemoryLimit = memoryLimit;
|
||||
await this.KubernetesResourceQuotaService.update(quota);
|
||||
} else {
|
||||
await this.createResourceQuotaAsync(namespace, owner, cpuLimit, memoryLimit);
|
||||
}
|
||||
} else if (quota) {
|
||||
await this.KubernetesResourceQuotaService.delete(quota);
|
||||
}
|
||||
this.Notifications.success('Resource pool successfully updated', this.pool.Namespace.Name);
|
||||
this.$state.reload();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to create resource pool');
|
||||
} finally {
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateResourcePool() {
|
||||
return this.$async(this.updateResourcePoolAsync);
|
||||
}
|
||||
|
||||
hasEventWarnings() {
|
||||
return this.state.eventWarningCount;
|
||||
}
|
||||
|
||||
async getEventsAsync() {
|
||||
try {
|
||||
this.state.eventsLoading = true;
|
||||
this.events = await this.KubernetesEventService.get(this.pool.Namespace.Name);
|
||||
this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve resource pool related events');
|
||||
} finally {
|
||||
this.state.eventsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getEvents() {
|
||||
return this.$async(this.getEventsAsync);
|
||||
}
|
||||
|
||||
async getApplicationsAsync() {
|
||||
try {
|
||||
this.state.applicationsLoading = true;
|
||||
this.applications = await this.KubernetesApplicationService.get(this.pool.Namespace.Name);
|
||||
this.applications = _.map(this.applications, (app) => {
|
||||
const resourceReservation = KubernetesResourceReservationHelper.computeResourceReservation(app.Pods);
|
||||
app.CPU = resourceReservation.CPU;
|
||||
app.Memory = resourceReservation.Memory;
|
||||
return app;
|
||||
});
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve applications.');
|
||||
} finally {
|
||||
this.state.applicationsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getApplications() {
|
||||
return this.$async(this.getApplicationsAsync);
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
try {
|
||||
this.isAdmin = this.Authentication.isAdmin();
|
||||
this.defaults = KubernetesResourceQuotaDefaults;
|
||||
|
||||
this.formValues = {
|
||||
MemoryLimit: this.defaults.MemoryLimit,
|
||||
CpuLimit: this.defaults.CpuLimit,
|
||||
hasQuota: false,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
actionInProgress: false,
|
||||
sliderMaxMemory: 0,
|
||||
sliderMaxCpu: 0,
|
||||
cpuUsage: 0,
|
||||
cpuUsed: 0,
|
||||
memoryUsage: 0,
|
||||
memoryUsed: 0,
|
||||
activeTab: 0,
|
||||
currentName: this.$state.$current.name,
|
||||
showEditorTab: false,
|
||||
eventsLoading: true,
|
||||
applicationsLoading: true,
|
||||
viewReady: false,
|
||||
eventWarningCount: 0,
|
||||
};
|
||||
|
||||
this.state.activeTab = this.LocalStorage.getActiveTab('resourcePool');
|
||||
|
||||
const name = this.$transition$.params().id;
|
||||
|
||||
const [nodes, pool] = await Promise.all([this.KubernetesNodeService.get(), this.KubernetesResourcePoolService.get(name)]);
|
||||
|
||||
this.pool = pool;
|
||||
|
||||
_.forEach(nodes, (item) => {
|
||||
this.state.sliderMaxMemory += filesizeParser(item.Memory);
|
||||
this.state.sliderMaxCpu += item.CPU;
|
||||
});
|
||||
this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory);
|
||||
|
||||
const quota = pool.Quota;
|
||||
if (quota) {
|
||||
this.formValues.hasQuota = true;
|
||||
this.formValues.CpuLimit = quota.CpuLimit;
|
||||
this.formValues.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimit);
|
||||
|
||||
this.state.cpuUsed = quota.CpuLimitUsed;
|
||||
this.state.memoryUsed = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimitUsed);
|
||||
}
|
||||
|
||||
await this.getEvents();
|
||||
await this.getApplications();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||
} finally {
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
if (this.state.currentName !== this.$state.$current.name) {
|
||||
this.LocalStorage.storeActiveTab('resourcePool', 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesResourcePoolController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesResourcePoolController', KubernetesResourcePoolController);
|
19
app/kubernetes/views/resource-pools/resourcePools.html
Normal file
19
app/kubernetes/views/resource-pools/resourcePools.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
<kubernetes-view-header title="Resource pool list" state="kubernetes.resourcePools" view-ready="ctrl.state.viewReady">
|
||||
Resource pools
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<kubernetes-resource-pools-datatable
|
||||
dataset="ctrl.resourcePools"
|
||||
table-key="kubernetes.resourcePools"
|
||||
order-by="Namespace.Name"
|
||||
remove-action="ctrl.removeAction"
|
||||
refresh-callback="ctrl.getResourcePools"
|
||||
></kubernetes-resource-pools-datatable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
5
app/kubernetes/views/resource-pools/resourcePools.js
Normal file
5
app/kubernetes/views/resource-pools/resourcePools.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesResourcePoolsView', {
|
||||
templateUrl: './resourcePools.html',
|
||||
controller: 'KubernetesResourcePoolsController',
|
||||
controllerAs: 'ctrl',
|
||||
});
|
|
@ -0,0 +1,77 @@
|
|||
import angular from 'angular';
|
||||
|
||||
class KubernetesResourcePoolsController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, Notifications, ModalService, KubernetesResourcePoolService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.Notifications = Notifications;
|
||||
this.ModalService = ModalService;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.getResourcePools = this.getResourcePools.bind(this);
|
||||
this.getResourcePoolsAsync = this.getResourcePoolsAsync.bind(this);
|
||||
this.removeAction = this.removeAction.bind(this);
|
||||
this.removeActionAsync = this.removeActionAsync.bind(this);
|
||||
}
|
||||
|
||||
async removeActionAsync(selectedItems) {
|
||||
let actionCount = selectedItems.length;
|
||||
for (const pool of selectedItems) {
|
||||
try {
|
||||
await this.KubernetesResourcePoolService.delete(pool);
|
||||
this.Notifications.success('Resource pool successfully removed', pool.Namespace.Name);
|
||||
const index = this.resourcePools.indexOf(pool);
|
||||
this.resourcePools.splice(index, 1);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to remove resource pool');
|
||||
} finally {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
this.$state.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeAction(selectedItems) {
|
||||
this.ModalService.confirmDeletion(
|
||||
'Do you want to remove the selected resource pool(s)? All the resources associated to the selected resource pool(s) will be removed too.',
|
||||
(confirmed) => {
|
||||
if (confirmed) {
|
||||
return this.$async(this.removeActionAsync, selectedItems);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async getResourcePoolsAsync() {
|
||||
try {
|
||||
this.resourcePools = await this.KubernetesResourcePoolService.get();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retreive resource pools');
|
||||
}
|
||||
}
|
||||
|
||||
getResourcePools() {
|
||||
return this.$async(this.getResourcePoolsAsync);
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
this.state = {
|
||||
viewReady: false,
|
||||
};
|
||||
|
||||
await this.getResourcePools();
|
||||
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesResourcePoolsController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesResourcePoolsController', KubernetesResourcePoolsController);
|
60
app/kubernetes/views/stacks/logs/logs.html
Normal file
60
app/kubernetes/views/stacks/logs/logs.html
Normal file
|
@ -0,0 +1,60 @@
|
|||
<kubernetes-view-header title="Stacks logs" state="kubernetes.stacks.stack.logs" view-ready="ctrl.state.viewReady">
|
||||
<a ui-sref="kubernetes.resourcePools">Resource pools</a>
|
||||
> <a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.state.transition.namespace })">{{ ctrl.state.transition.namespace }}</a> >
|
||||
<a ui-sref="kubernetes.applications">Applications</a> > Stacks > {{ ctrl.state.transition.name }} > Logs
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady" style="height: 100%;">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" autocomplete="off">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<!-- auto-refresh -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Auto-refresh
|
||||
<portainer-tooltip position="bottom" message="Automatically refresh logs every 30 seconds"></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.state.autoRefresh" ng-change="ctrl.updateAutoRefresh()" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !auto-refresh -->
|
||||
<!-- search -->
|
||||
<div class="form-group">
|
||||
<label for="logs_search" class="col-sm-1 control-label text-left">
|
||||
Search
|
||||
</label>
|
||||
<div class="col-sm-11">
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="logs_search"
|
||||
ng-model="ctrl.state.search"
|
||||
ng-change="ctrl.state.selectedLines.length = 0;"
|
||||
placeholder="Filter..."
|
||||
auto-focus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !search -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="height: 54%;">
|
||||
<div class="col-sm-12" style="height: 100%;">
|
||||
<pre
|
||||
class="log_viewer"
|
||||
><div ng-repeat="line in ctrl.state.filteredLogs = (ctrl.stackLogs | filter:ctrl.state.search) track by $index" class="line" ng-if="line"><p class="inner_line"><span ng-style="{'color': line.Color, 'font-weight': 'bold'};">{{ line.AppName }}</span> {{ line.Line }}</p></div><div ng-if="ctrl.stackLogs.length && !ctrl.state.filteredLogs.length" class="line"><p class="inner_line">No log line matching the '{{ ctrl.state.search }}' filter</p></div><div ng-if="ctrl.stackLogs.length === 0" class="line"><p class="inner_line">No logs available</p></div></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
8
app/kubernetes/views/stacks/logs/logs.js
Normal file
8
app/kubernetes/views/stacks/logs/logs.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesStackLogsView', {
|
||||
templateUrl: './logs.html',
|
||||
controller: 'KubernetesStackLogsController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
},
|
||||
});
|
122
app/kubernetes/views/stacks/logs/logsController.js
Normal file
122
app/kubernetes/views/stacks/logs/logsController.js
Normal file
|
@ -0,0 +1,122 @@
|
|||
import _ from 'lodash-es';
|
||||
import angular from 'angular';
|
||||
import $allSettled from 'Portainer/services/allSettled';
|
||||
|
||||
const colors = ['red', 'orange', 'lime', 'green', 'darkgreen', 'cyan', 'turquoise', 'teal', 'deepskyblue', 'blue', 'darkblue', 'slateblue', 'magenta', 'darkviolet'];
|
||||
|
||||
class KubernetesStackLogsController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, $interval, Notifications, KubernetesApplicationService, KubernetesPodService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.$interval = $interval;
|
||||
this.Notifications = Notifications;
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
this.KubernetesPodService = KubernetesPodService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.stopRepeater = this.stopRepeater.bind(this);
|
||||
this.generateLogsPromise = this.generateLogsPromise.bind(this);
|
||||
this.generateAppPromise = this.generateAppPromise.bind(this);
|
||||
this.getStackLogsAsync = this.getStackLogsAsync.bind(this);
|
||||
}
|
||||
|
||||
updateAutoRefresh() {
|
||||
if (this.state.autoRefresh) {
|
||||
this.setUpdateRepeater();
|
||||
return;
|
||||
}
|
||||
|
||||
this.stopRepeater();
|
||||
}
|
||||
|
||||
stopRepeater() {
|
||||
if (angular.isDefined(this.repeater)) {
|
||||
this.$interval.cancel(this.repeater);
|
||||
this.repeater = null;
|
||||
}
|
||||
}
|
||||
|
||||
setUpdateRepeater() {
|
||||
this.repeater = this.$interval(this.getStackLogsAsync, this.state.refreshRate);
|
||||
}
|
||||
|
||||
async generateLogsPromise(pod) {
|
||||
const res = {
|
||||
Pod: pod,
|
||||
Logs: [],
|
||||
};
|
||||
res.Logs = await this.KubernetesPodService.logs(pod.Namespace, pod.Name);
|
||||
return res;
|
||||
}
|
||||
|
||||
async generateAppPromise(app) {
|
||||
const res = {
|
||||
Application: app,
|
||||
Pods: [],
|
||||
};
|
||||
|
||||
const promises = _.map(app.Pods, this.generateLogsPromise);
|
||||
const result = await $allSettled(promises);
|
||||
res.Pods = result.fulfilled;
|
||||
return res;
|
||||
}
|
||||
|
||||
async getStackLogsAsync() {
|
||||
try {
|
||||
const applications = await this.KubernetesApplicationService.get(this.state.transition.namespace);
|
||||
const filteredApplications = _.filter(applications, (app) => app.StackName === this.state.transition.name);
|
||||
const logsPromises = _.map(filteredApplications, this.generateAppPromise);
|
||||
const data = await Promise.all(logsPromises);
|
||||
const logs = _.flatMap(data, (app, index) => {
|
||||
return _.flatMap(app.Pods, (pod) => {
|
||||
return _.map(pod.Logs, (line) => {
|
||||
const res = {
|
||||
Color: colors[index % colors.length],
|
||||
Line: line,
|
||||
AppName: pod.Pod.Name,
|
||||
};
|
||||
return res;
|
||||
});
|
||||
});
|
||||
});
|
||||
this.stackLogs = logs;
|
||||
} catch (err) {
|
||||
this.stopRepeater();
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve application logs');
|
||||
}
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
this.state = {
|
||||
autoRefresh: false,
|
||||
refreshRate: 30000, // 30 seconds
|
||||
search: '',
|
||||
viewReady: false,
|
||||
transition: {
|
||||
namespace: this.$transition$.params().namespace,
|
||||
name: this.$transition$.params().name,
|
||||
},
|
||||
};
|
||||
|
||||
this.stackLogs = [];
|
||||
try {
|
||||
await this.getStackLogsAsync();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve stack logs');
|
||||
} finally {
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
this.stopRepeater();
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesStackLogsController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesStackLogsController', KubernetesStackLogsController);
|
99
app/kubernetes/views/volumes/edit/volume.html
Normal file
99
app/kubernetes/views/volumes/edit/volume.html
Normal file
|
@ -0,0 +1,99 @@
|
|||
<kubernetes-view-header title="Volume details" state="kubernetes.volumes.volume" view-ready="ctrl.state.viewReady">
|
||||
<a ui-sref="kubernetes.resourcePools">Resource pools</a> >
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.volume.ResourcePool.Namespace.Name })">{{ ctrl.volume.ResourcePool.Namespace.Name }}</a> >
|
||||
<a ui-sref="kubernetes.volumes">Volumes</a> > {{ ctrl.volume.PersistentVolumeClaim.Name }}
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<uib-tabset active="ctrl.state.activeTab" justified="true" type="pills">
|
||||
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)">
|
||||
<uib-tab-heading> <i class="fa fa-database space-right" aria-hidden="true"></i> Volume </uib-tab-heading>
|
||||
|
||||
<div style="padding: 20px;">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
{{ ctrl.volume.PersistentVolumeClaim.Name }}
|
||||
<span class="label label-primary image-tag label-margins" ng-if="!ctrl.isSystemNamespace() && ctrl.isExternalVolume()">external</span>
|
||||
<span class="label label-warning image-tag label-margins" ng-if="!ctrl.isSystemNamespace() && !ctrl.isUsed()">unused</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Resource pool</td>
|
||||
<td>
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.volume.ResourcePool.Namespace.Name })">{{ ctrl.volume.ResourcePool.Namespace.Name }}</a>
|
||||
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="ctrl.isSystemNamespace(item)">system</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Storage</td>
|
||||
<td>{{ ctrl.volume.PersistentVolumeClaim.StorageClass.Name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Size</td>
|
||||
<td>{{ ctrl.volume.PersistentVolumeClaim.Storage }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Creation date</td>
|
||||
<td>{{ ctrl.volume.PersistentVolumeClaim.CreationDate | getisodate }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)">
|
||||
<uib-tab-heading>
|
||||
<i class="fa fa-history space-right" aria-hidden="true"></i> Events
|
||||
<div ng-if="ctrl.hasEventWarnings()">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
{{ ctrl.state.eventWarningCount }} warning(s)
|
||||
</div>
|
||||
</uib-tab-heading>
|
||||
<kubernetes-events-datatable
|
||||
title-text="Events"
|
||||
title-icon="fa-history"
|
||||
dataset="ctrl.events"
|
||||
table-key="kubernetes.volume.events"
|
||||
order-by="Date"
|
||||
reverse-order="true"
|
||||
loading="ctrl.state.eventsLoading"
|
||||
refresh-callback="ctrl.getEvents"
|
||||
>
|
||||
</kubernetes-events-datatable>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="2" ng-if="ctrl.volume.PersistentVolumeClaim.Yaml" select="ctrl.showEditor()" classes="btn-sm">
|
||||
<uib-tab-heading> <i class="fa fa-code space-right" aria-hidden="true"></i> YAML </uib-tab-heading>
|
||||
<div style="padding-right: 25px;" ng-if="ctrl.state.showEditorTab">
|
||||
<kubernetes-yaml-inspector key="volume-yaml" data="ctrl.volume.PersistentVolumeClaim.Yaml"></kubernetes-yaml-inspector>
|
||||
</div>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="ctrl.isUsed(item)">
|
||||
<div class="col-sm-12">
|
||||
<kubernetes-integrated-applications-datatable
|
||||
dataset="ctrl.volume.Applications"
|
||||
table-key="kubernetes.volume.applications"
|
||||
order-by="Name"
|
||||
refresh-callback="ctrl.getVolume"
|
||||
title-text="Applications using this volume"
|
||||
title-icon="fa-laptop-code"
|
||||
>
|
||||
</kubernetes-integrated-applications-datatable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
8
app/kubernetes/views/volumes/edit/volume.js
Normal file
8
app/kubernetes/views/volumes/edit/volume.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesVolumeView', {
|
||||
templateUrl: './volume.html',
|
||||
controller: 'KubernetesVolumeController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
},
|
||||
});
|
130
app/kubernetes/views/volumes/edit/volumeController.js
Normal file
130
app/kubernetes/views/volumes/edit/volumeController.js
Normal file
|
@ -0,0 +1,130 @@
|
|||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
|
||||
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
||||
|
||||
class KubernetesVolumeController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, Notifications, LocalStorage, KubernetesVolumeService, KubernetesEventService, KubernetesNamespaceHelper, KubernetesApplicationService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.Notifications = Notifications;
|
||||
this.LocalStorage = LocalStorage;
|
||||
|
||||
this.KubernetesVolumeService = KubernetesVolumeService;
|
||||
this.KubernetesEventService = KubernetesEventService;
|
||||
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.getVolume = this.getVolume.bind(this);
|
||||
this.getVolumeAsync = this.getVolumeAsync.bind(this);
|
||||
this.getEvents = this.getEvents.bind(this);
|
||||
this.getEventsAsync = this.getEventsAsync.bind(this);
|
||||
}
|
||||
|
||||
selectTab(index) {
|
||||
this.LocalStorage.storeActiveTab('volume', index);
|
||||
}
|
||||
|
||||
showEditor() {
|
||||
this.state.showEditorTab = true;
|
||||
this.selectTab(2);
|
||||
}
|
||||
|
||||
isExternalVolume() {
|
||||
return KubernetesVolumeHelper.isExternalVolume(this.volume);
|
||||
}
|
||||
|
||||
isSystemNamespace() {
|
||||
return this.KubernetesNamespaceHelper.isSystemNamespace(this.volume.ResourcePool.Namespace.Name);
|
||||
}
|
||||
|
||||
isUsed() {
|
||||
return KubernetesVolumeHelper.isUsed(this.volume);
|
||||
}
|
||||
|
||||
/**
|
||||
* VOLUME
|
||||
*/
|
||||
async getVolumeAsync() {
|
||||
try {
|
||||
const [volume, applications] = await Promise.all([
|
||||
this.KubernetesVolumeService.get(this.state.namespace, this.state.name),
|
||||
this.KubernetesApplicationService.get(this.state.namespace),
|
||||
]);
|
||||
volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications);
|
||||
this.volume = volume;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve volume');
|
||||
}
|
||||
}
|
||||
|
||||
getVolume() {
|
||||
return this.$async(this.getVolumeAsync);
|
||||
}
|
||||
|
||||
/**
|
||||
* EVENTS
|
||||
*/
|
||||
hasEventWarnings() {
|
||||
return this.state.eventWarningCount;
|
||||
}
|
||||
|
||||
async getEventsAsync() {
|
||||
try {
|
||||
this.state.eventsLoading = true;
|
||||
const events = await this.KubernetesEventService.get(this.state.namespace);
|
||||
this.events = _.filter(events, (event) => event.Involved.uid === this.volume.PersistentVolumeClaim.Id);
|
||||
this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve application related events');
|
||||
} finally {
|
||||
this.state.eventsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getEvents() {
|
||||
return this.$async(this.getEventsAsync);
|
||||
}
|
||||
|
||||
/**
|
||||
* ON INIT
|
||||
*/
|
||||
async onInit() {
|
||||
this.state = {
|
||||
activeTab: 0,
|
||||
currentName: this.$state.$current.name,
|
||||
showEditorTab: false,
|
||||
eventsLoading: true,
|
||||
viewReady: false,
|
||||
namespace: this.$transition$.params().namespace,
|
||||
name: this.$transition$.params().name,
|
||||
eventWarningCount: 0,
|
||||
};
|
||||
|
||||
this.state.activeTab = this.LocalStorage.getActiveTab('volume');
|
||||
|
||||
try {
|
||||
await this.getVolume();
|
||||
await this.getEvents();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||
} finally {
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
if (this.state.currentName !== this.$state.$current.name) {
|
||||
this.LocalStorage.storeActiveTab('volume', 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesVolumeController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesVolumeController', KubernetesVolumeController);
|
20
app/kubernetes/views/volumes/volumes.html
Normal file
20
app/kubernetes/views/volumes/volumes.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
<kubernetes-view-header title="Volume list" state="kubernetes.volumes" view-ready="ctrl.state.viewReady">
|
||||
Volumes
|
||||
</kubernetes-view-header>
|
||||
|
||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
||||
|
||||
<div ng-if="ctrl.state.viewReady">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<kubernetes-volumes-datatable
|
||||
dataset="ctrl.volumes"
|
||||
table-key="kubernetes.volumes"
|
||||
order-by="PersistentVolumeClaim.Name"
|
||||
remove-action="ctrl.removeAction"
|
||||
refresh-callback="ctrl.getVolumes"
|
||||
>
|
||||
</kubernetes-volumes-datatable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
5
app/kubernetes/views/volumes/volumes.js
Normal file
5
app/kubernetes/views/volumes/volumes.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
angular.module('portainer.kubernetes').component('kubernetesVolumesView', {
|
||||
templateUrl: './volumes.html',
|
||||
controller: 'KubernetesVolumesController',
|
||||
controllerAs: 'ctrl',
|
||||
});
|
82
app/kubernetes/views/volumes/volumesController.js
Normal file
82
app/kubernetes/views/volumes/volumesController.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
import _ from 'lodash-es';
|
||||
import angular from 'angular';
|
||||
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
|
||||
|
||||
class KubernetesVolumesController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, Notifications, ModalService, KubernetesVolumeService, KubernetesApplicationService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.Notifications = Notifications;
|
||||
this.ModalService = ModalService;
|
||||
this.KubernetesVolumeService = KubernetesVolumeService;
|
||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.getVolumes = this.getVolumes.bind(this);
|
||||
this.getVolumesAsync = this.getVolumesAsync.bind(this);
|
||||
this.removeAction = this.removeAction.bind(this);
|
||||
this.removeActionAsync = this.removeActionAsync.bind(this);
|
||||
}
|
||||
|
||||
async removeActionAsync(selectedItems) {
|
||||
let actionCount = selectedItems.length;
|
||||
for (const volume of selectedItems) {
|
||||
try {
|
||||
await this.KubernetesVolumeService.delete(volume);
|
||||
this.Notifications.success('Volume successfully removed', volume.PersistentVolumeClaim.Name);
|
||||
const index = this.volumes.indexOf(volume);
|
||||
this.volumes.splice(index, 1);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to remove volume');
|
||||
} finally {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
this.$state.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeAction(selectedItems) {
|
||||
this.ModalService.confirmDeletion('Do you want to remove the selected volume(s)?', (confirmed) => {
|
||||
if (confirmed) {
|
||||
return this.$async(this.removeActionAsync, selectedItems);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getVolumesAsync() {
|
||||
try {
|
||||
const [volumes, applications] = await Promise.all([this.KubernetesVolumeService.get(), this.KubernetesApplicationService.get()]);
|
||||
|
||||
this.volumes = _.map(volumes, (volume) => {
|
||||
volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications);
|
||||
return volume;
|
||||
});
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retreive resource pools');
|
||||
}
|
||||
}
|
||||
|
||||
getVolumes() {
|
||||
return this.$async(this.getVolumesAsync);
|
||||
}
|
||||
|
||||
async onInit() {
|
||||
this.state = {
|
||||
viewReady: false,
|
||||
};
|
||||
|
||||
await this.getVolumes();
|
||||
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesVolumesController;
|
||||
angular.module('portainer.kubernetes').controller('KubernetesVolumesController', KubernetesVolumesController);
|
Loading…
Add table
Add a link
Reference in a new issue